Extension: Castle (Accounting) #12

Open
opened 2026-04-01 16:39:56 +00:00 by padreug · 1 comment
Owner

Summary

Implement a Nostr-native double-entry accounting extension for Lightning.Pub, replacing the LNbits castle extension. This is the most complex extension to port and should be approached incrementally.

Motivation

Castle provides double-entry bookkeeping for collectives (co-living spaces, makerspaces, community projects). The LNbits version depends on an external Fava/Beancount Python service for all accounting calculations — an awkward dependency that ties it to HTTP and a separate process.

A Lightning.Pub extension could:

  • Handle balance queries as Nostr RPC calls from any device
  • Accept expense submissions from mobile Nostr clients
  • Run approval workflows via encrypted Nostr DMs
  • Create tamper-evident audit trails as signed Nostr events

Key Design Decision: Fava Dependency

The LNbits version delegates all accounting math to Fava/Beancount. Options for LP:

  1. Keep Fava — fastest to implement but defeats the "pure Nostr" goal and requires a sidecar Python service
  2. Implement accounting in TypeScript — significant effort but fully self-contained; only needs balanced debit/credit enforcement and hierarchical account sums
  3. Start simple, grow incrementally — recommended approach (see phased plan below)

Phased Implementation

Phase 1: SATS-only accounting (MVP)

  • Flat account list (no hierarchy)
  • Single currency (SATS)
  • Basic journal entries with balanced debits/credits
  • User balances computed from journal entries
  • Invoice generation for payments
  • ~1500-2000 lines

Phase 2: Multi-currency + hierarchy

  • Hierarchical accounts with : separator (Expenses:Food:Groceries)
  • Fiat currency tracking with cost basis notation
  • Permission inheritance on account hierarchy
  • ~1500 lines additional

Phase 3: Full feature parity

  • RBAC permission system (READ, SUBMIT_EXPENSE, MANAGE)
  • Manual payment request/approval workflow via Nostr DMs
  • Balance assertions and reconciliation
  • CSV/JSON export
  • ~2000 lines additional

Scope (Full)

RPC Methods:

castle.createAccount        castle.listAccounts
castle.createExpense        castle.createReceivable
castle.createRevenue        castle.createEntry
castle.getBalance           castle.getAllBalances
castle.getUserEntries       castle.generateInvoice
castle.recordPayment        castle.settleReceivable
castle.payUser              castle.requestPayment
castle.approvePayment       castle.rejectPayment
castle.listPaymentRequests  castle.getSettings
castle.updateSettings       castle.grantPermission
castle.revokePermission     castle.reconcile

Data Model:

  • Account — name, type (Asset/Liability/Equity/Revenue/Expense), parent, metadata
  • JournalEntry — date, description, flag (cleared/pending/void), reference, creator_pubkey
  • EntryLine — entry_id, account_id, amount_sats, fiat_amount, fiat_currency, fiat_rate
  • AccountPermission — account_id, user_pubkey, permission_type, expires_at, granted_by
  • PaymentRequest — user_pubkey, amount, description, status, approved_by

Core Accounting Invariants:

  1. Every entry must have balanced debits and credits
  2. Fiat amounts tracked via metadata, NOT converted from current sats balance
  3. User identity via Nostr pubkey (not wallet ID)
  4. All calculations done in-extension (no external dependency)

Complexity

HIGH — ~5000-8000 lines total across all phases. 2-4 weeks for full implementation.

The accounting logic itself is well-defined (double-entry is centuries old), but currency handling, permission hierarchies, and reconciliation features add significant scope.

Nostr-Native Advantages

Feature LNbits (HTTP) Lightning.Pub (Nostr)
Expense submission Web UI only Any Nostr client
Balance check API call with key Nostr RPC from phone
Approval workflow Web UI click Encrypted DM reply
Audit trail Database rows Signed Nostr events
Multi-device Share API key Same Nostr keypair
Notifications None Nostr DM on expense/payment

Reference

  • LNbits castle extension: lnbits/extensions/castle/
  • Castle CLAUDE.md: comprehensive architecture docs
  • Beancount format patterns: castle/beancount_format.py
  • Permission system: castle/docs/PERMISSIONS-SYSTEM.md
  • BQL query patterns: castle/docs/BQL-BALANCE-QUERIES.md
## Summary Implement a Nostr-native double-entry accounting extension for Lightning.Pub, replacing the LNbits `castle` extension. This is the most complex extension to port and should be approached incrementally. ## Motivation Castle provides double-entry bookkeeping for collectives (co-living spaces, makerspaces, community projects). The LNbits version depends on an external Fava/Beancount Python service for all accounting calculations — an awkward dependency that ties it to HTTP and a separate process. A Lightning.Pub extension could: - Handle balance queries as Nostr RPC calls from any device - Accept expense submissions from mobile Nostr clients - Run approval workflows via encrypted Nostr DMs - Create tamper-evident audit trails as signed Nostr events ## Key Design Decision: Fava Dependency The LNbits version delegates all accounting math to Fava/Beancount. Options for LP: 1. **Keep Fava** — fastest to implement but defeats the "pure Nostr" goal and requires a sidecar Python service 2. **Implement accounting in TypeScript** — significant effort but fully self-contained; only needs balanced debit/credit enforcement and hierarchical account sums 3. **Start simple, grow incrementally** — recommended approach (see phased plan below) ## Phased Implementation ### Phase 1: SATS-only accounting (MVP) - Flat account list (no hierarchy) - Single currency (SATS) - Basic journal entries with balanced debits/credits - User balances computed from journal entries - Invoice generation for payments - ~1500-2000 lines ### Phase 2: Multi-currency + hierarchy - Hierarchical accounts with `:` separator (`Expenses:Food:Groceries`) - Fiat currency tracking with cost basis notation - Permission inheritance on account hierarchy - ~1500 lines additional ### Phase 3: Full feature parity - RBAC permission system (READ, SUBMIT_EXPENSE, MANAGE) - Manual payment request/approval workflow via Nostr DMs - Balance assertions and reconciliation - CSV/JSON export - ~2000 lines additional ## Scope (Full) **RPC Methods:** ``` castle.createAccount castle.listAccounts castle.createExpense castle.createReceivable castle.createRevenue castle.createEntry castle.getBalance castle.getAllBalances castle.getUserEntries castle.generateInvoice castle.recordPayment castle.settleReceivable castle.payUser castle.requestPayment castle.approvePayment castle.rejectPayment castle.listPaymentRequests castle.getSettings castle.updateSettings castle.grantPermission castle.revokePermission castle.reconcile ``` **Data Model:** - `Account` — name, type (Asset/Liability/Equity/Revenue/Expense), parent, metadata - `JournalEntry` — date, description, flag (cleared/pending/void), reference, creator_pubkey - `EntryLine` — entry_id, account_id, amount_sats, fiat_amount, fiat_currency, fiat_rate - `AccountPermission` — account_id, user_pubkey, permission_type, expires_at, granted_by - `PaymentRequest` — user_pubkey, amount, description, status, approved_by **Core Accounting Invariants:** 1. Every entry must have balanced debits and credits 2. Fiat amounts tracked via metadata, NOT converted from current sats balance 3. User identity via Nostr pubkey (not wallet ID) 4. All calculations done in-extension (no external dependency) ## Complexity **HIGH** — ~5000-8000 lines total across all phases. 2-4 weeks for full implementation. The accounting logic itself is well-defined (double-entry is centuries old), but currency handling, permission hierarchies, and reconciliation features add significant scope. ## Nostr-Native Advantages | Feature | LNbits (HTTP) | Lightning.Pub (Nostr) | |---------|--------------|----------------------| | Expense submission | Web UI only | Any Nostr client | | Balance check | API call with key | Nostr RPC from phone | | Approval workflow | Web UI click | Encrypted DM reply | | Audit trail | Database rows | Signed Nostr events | | Multi-device | Share API key | Same Nostr keypair | | Notifications | None | Nostr DM on expense/payment | ## Reference - LNbits castle extension: `lnbits/extensions/castle/` - Castle CLAUDE.md: comprehensive architecture docs - Beancount format patterns: `castle/beancount_format.py` - Permission system: `castle/docs/PERMISSIONS-SYSTEM.md` - BQL query patterns: `castle/docs/BQL-BALANCE-QUERIES.md`
Author
Owner

NIP Review: Applicable Standards for Castle Extension

Castle has the richest NIP surface area of the three extensions. The Nostr protocol provides building blocks for every major accounting concern: authorization, audit trails, encrypted communication, role management, and payment settlement.

Core NIPs to Implement

NIP-47: Nostr Wallet Connect — SETTLEMENT ENGINE

Kinds: 13194, 23194, 23195, 23197

Maps directly to accounting operations:

NWC Method Accounting Use
make_invoice Create receivables (user owes Castle)
pay_invoice Settle payables (Castle pays user)
list_transactions Authoritative transaction feed for ledger entries
get_balance Cash position reconciliation
make_hold_invoice / settle_hold_invoice Accrual accounting — escrow/pending states before settlement

Key insight: Hold invoices enable proper accrual accounting. Create hold invoice = record receivable; settle = recognize cash receipt. Cancel = write off. This maps directly to Castle's "pending → cleared" journal entry lifecycle.

The fees_paid field from responses directly populates the expense ledger for Lightning network costs — no manual fee tracking needed.

NIP-78: Application-Specific Data — CHART OF ACCOUNTS

Kind: 30078

Store the entire chart of accounts and Castle configuration as encrypted addressable events:

{
  "kind": 30078,
  "tags": [["d", "castle/chart-of-accounts"]],
  "content": "nip44_encrypt({
    'accounts': [
      {'code': '1000', 'name': 'Assets:Lightning:Balance', 'type': 'asset'},
      {'code': '2000', 'name': 'Liabilities:Payable:User-abc', 'type': 'liability'},
      {'code': '4000', 'name': 'Income:Accommodation', 'type': 'revenue'},
      {'code': '5000', 'name': 'Expenses:Food', 'type': 'expense'}
    ]
  })"
}

Also use separate d tags for settings (castle/settings), reporting period snapshots, and journal entry templates. Being addressable and replaceable means the chart evolves without event proliferation.

NIP-17: Private Direct Messages — APPROVAL WORKFLOWS

Kinds: 14 (chat), 15 (file), 1059 (gift wrap), 13 (seal)

This is how Castle replaces its web-only approval UI with a protocol-native workflow:

  1. User submits expense → Extension creates sealed DM (kind 14 → 13 → 1059) to admin
  2. Admin reviews → Decrypts, sees amount/vendor/account, replies with approval/rejection
  3. On approval → Extension triggers NWC payment and records journal entry
  4. File attachments → kind 15 for receipt images/PDFs with SHA-256 integrity hash

The three-layer structure (rumor → seal → gift wrap) means relays never see who is approving what — critical for financial privacy.

NIP-59: Gift Wrap — METADATA PROTECTION

Kinds: 13 (seal), 1059 (gift wrap)

Wraps all sensitive financial communications. Temporal obfuscation (randomized created_at up to 2 days) prevents correlation attacks like "payment arrived → 2 minutes later approval was sent." Each approval gets a unique ephemeral key.

NIP-58: Badges — ROLE-BASED ACCESS CONTROL

Kinds: 30009 (badge definition), 8 (badge award), 30008 (profile badges)

Replace Castle's database RBAC with Nostr-native roles:

// Define role
{ "kind": 30009, "tags": [["d", "castle-accountant"], ["name", "Accountant"], ["description", "Can record journal entries"]] }

// Award role
{ "kind": 8, "tags": [["a", "30009:<castle-pubkey>:castle-accountant"], ["p", "<user-pubkey>"]] }

Roles to define:

  • castle-admin — Full access (super user)
  • castle-accountant — Record entries, view balances
  • castle-approver — Approve expenses and payment requests
  • castle-viewer — Read-only balance and report access

Badges are non-transferable and cryptographically signed — audit-proof role assignment.

NIP-32: Labeling — TRANSACTION CLASSIFICATION

Kind: 1985

Tag journal entries with accounting metadata:

{
  "kind": 1985,
  "tags": [
    ["L", "castle.cost-center"],
    ["l", "kitchen", "castle.cost-center"],
    ["L", "castle.expense-type"],
    ["l", "supplies", "castle.expense-type"],
    ["e", "<journal-entry-event-id>"]
  ]
}

Enables slicing the ledger by cost center, department, vendor, project — all queryable via standard Nostr filters.

Supplementary NIPs

NIP Kind Use Case Priority
NIP-44 (Encryption) All encrypted data uses ChaCha20+HMAC; mandatory Required
NIP-51 (Lists) 30000 Permission sets — which pubkeys can access which accounts Recommended
NIP-57 (Zaps) 9735 Zap receipts as payment evidence for revenue entries Recommended
NIP-56 (Reporting) 1984 Flag suspicious entries (fraud, duplicate, policy violation) Optional
NIP-40 (Expiration) Temporary permissions with auto-expiry Optional
NIP-84 (Highlights) 9802 Auditor highlights material transactions for review Nice-to-have
NIP-B7 (Blossom) 10063 Decentralized receipt/invoice PDF storage with SHA-256 verification Future
NIP-EE (MLS Groups) 443-445 Scalable group encryption for large finance teams Future

Double-Entry Architecture Using NIPs

Journal entry as a composite Nostr structure:

  • Journal entry header stored in extension DB (or as encrypted NIP-78 event)
  • Each entry line tagged with NIP-32 labels (cost center, account code)
  • Payment settlement via NIP-47 NWC
  • Approval chain via NIP-17 sealed DMs
  • Roles verified via NIP-58 badges
  • Audit queries via standard Nostr tag filters (#e, #p, #a, #l)

Settlement flow:

User submits expense (NIP-17 DM to admin)
  → Admin approves (NIP-17 reply, verified by NIP-58 badge)
    → Extension creates journal entry (DR Expense, CR Liability)
      → When Castle pays user: NWC pay_invoice (NIP-47)
        → Record settlement entry (DR Liability, CR Assets:Lightning)
          → Tag with NIP-32 labels for reporting

What replaces Fava/Beancount:

  • Balance calculations: SUM of entry lines grouped by account in SQLite (no external service)
  • Trial balance: Query all entry lines, verify debits = credits
  • Account hierarchy: : separator parsed in TypeScript, permission inheritance via string prefix matching
  • Cost basis tracking: Store fiat metadata in entry line fields, never convert from current sats rate

Summary

Castle benefits from the most NIPs because accounting touches identity (NIP-58), communication (NIP-17/59), payments (NIP-47/57), data storage (NIP-78), classification (NIP-32), and access control (NIP-51). The protocol provides every primitive needed — the extension's job is to enforce double-entry invariants and orchestrate the workflow.

## NIP Review: Applicable Standards for Castle Extension Castle has the richest NIP surface area of the three extensions. The Nostr protocol provides building blocks for every major accounting concern: authorization, audit trails, encrypted communication, role management, and payment settlement. ### Core NIPs to Implement #### NIP-47: Nostr Wallet Connect — SETTLEMENT ENGINE **Kinds:** 13194, 23194, 23195, 23197 Maps directly to accounting operations: | NWC Method | Accounting Use | |-----------|---------------| | `make_invoice` | Create receivables (user owes Castle) | | `pay_invoice` | Settle payables (Castle pays user) | | `list_transactions` | Authoritative transaction feed for ledger entries | | `get_balance` | Cash position reconciliation | | `make_hold_invoice` / `settle_hold_invoice` | Accrual accounting — escrow/pending states before settlement | **Key insight:** Hold invoices enable proper accrual accounting. Create hold invoice = record receivable; settle = recognize cash receipt. Cancel = write off. This maps directly to Castle's "pending → cleared" journal entry lifecycle. The `fees_paid` field from responses directly populates the expense ledger for Lightning network costs — no manual fee tracking needed. #### NIP-78: Application-Specific Data — CHART OF ACCOUNTS **Kind:** 30078 Store the entire chart of accounts and Castle configuration as encrypted addressable events: ```json { "kind": 30078, "tags": [["d", "castle/chart-of-accounts"]], "content": "nip44_encrypt({ 'accounts': [ {'code': '1000', 'name': 'Assets:Lightning:Balance', 'type': 'asset'}, {'code': '2000', 'name': 'Liabilities:Payable:User-abc', 'type': 'liability'}, {'code': '4000', 'name': 'Income:Accommodation', 'type': 'revenue'}, {'code': '5000', 'name': 'Expenses:Food', 'type': 'expense'} ] })" } ``` Also use separate `d` tags for settings (`castle/settings`), reporting period snapshots, and journal entry templates. Being addressable and replaceable means the chart evolves without event proliferation. #### NIP-17: Private Direct Messages — APPROVAL WORKFLOWS **Kinds:** 14 (chat), 15 (file), 1059 (gift wrap), 13 (seal) This is how Castle replaces its web-only approval UI with a protocol-native workflow: 1. **User submits expense** → Extension creates sealed DM (kind 14 → 13 → 1059) to admin 2. **Admin reviews** → Decrypts, sees amount/vendor/account, replies with approval/rejection 3. **On approval** → Extension triggers NWC payment and records journal entry 4. **File attachments** → kind 15 for receipt images/PDFs with SHA-256 integrity hash The three-layer structure (rumor → seal → gift wrap) means relays never see who is approving what — critical for financial privacy. #### NIP-59: Gift Wrap — METADATA PROTECTION **Kinds:** 13 (seal), 1059 (gift wrap) Wraps all sensitive financial communications. Temporal obfuscation (randomized `created_at` up to 2 days) prevents correlation attacks like "payment arrived → 2 minutes later approval was sent." Each approval gets a unique ephemeral key. #### NIP-58: Badges — ROLE-BASED ACCESS CONTROL **Kinds:** 30009 (badge definition), 8 (badge award), 30008 (profile badges) Replace Castle's database RBAC with Nostr-native roles: ```json // Define role { "kind": 30009, "tags": [["d", "castle-accountant"], ["name", "Accountant"], ["description", "Can record journal entries"]] } // Award role { "kind": 8, "tags": [["a", "30009:<castle-pubkey>:castle-accountant"], ["p", "<user-pubkey>"]] } ``` **Roles to define:** - `castle-admin` — Full access (super user) - `castle-accountant` — Record entries, view balances - `castle-approver` — Approve expenses and payment requests - `castle-viewer` — Read-only balance and report access Badges are non-transferable and cryptographically signed — audit-proof role assignment. #### NIP-32: Labeling — TRANSACTION CLASSIFICATION **Kind:** 1985 Tag journal entries with accounting metadata: ```json { "kind": 1985, "tags": [ ["L", "castle.cost-center"], ["l", "kitchen", "castle.cost-center"], ["L", "castle.expense-type"], ["l", "supplies", "castle.expense-type"], ["e", "<journal-entry-event-id>"] ] } ``` Enables slicing the ledger by cost center, department, vendor, project — all queryable via standard Nostr filters. ### Supplementary NIPs | NIP | Kind | Use Case | Priority | |-----|------|----------|----------| | **NIP-44** (Encryption) | — | All encrypted data uses ChaCha20+HMAC; mandatory | Required | | **NIP-51** (Lists) | 30000 | Permission sets — which pubkeys can access which accounts | Recommended | | **NIP-57** (Zaps) | 9735 | Zap receipts as payment evidence for revenue entries | Recommended | | **NIP-56** (Reporting) | 1984 | Flag suspicious entries (fraud, duplicate, policy violation) | Optional | | **NIP-40** (Expiration) | — | Temporary permissions with auto-expiry | Optional | | **NIP-84** (Highlights) | 9802 | Auditor highlights material transactions for review | Nice-to-have | | **NIP-B7** (Blossom) | 10063 | Decentralized receipt/invoice PDF storage with SHA-256 verification | Future | | **NIP-EE** (MLS Groups) | 443-445 | Scalable group encryption for large finance teams | Future | ### Double-Entry Architecture Using NIPs **Journal entry as a composite Nostr structure:** - Journal entry header stored in extension DB (or as encrypted NIP-78 event) - Each entry line tagged with NIP-32 labels (cost center, account code) - Payment settlement via NIP-47 NWC - Approval chain via NIP-17 sealed DMs - Roles verified via NIP-58 badges - Audit queries via standard Nostr tag filters (`#e`, `#p`, `#a`, `#l`) **Settlement flow:** ``` User submits expense (NIP-17 DM to admin) → Admin approves (NIP-17 reply, verified by NIP-58 badge) → Extension creates journal entry (DR Expense, CR Liability) → When Castle pays user: NWC pay_invoice (NIP-47) → Record settlement entry (DR Liability, CR Assets:Lightning) → Tag with NIP-32 labels for reporting ``` **What replaces Fava/Beancount:** - Balance calculations: SUM of entry lines grouped by account in SQLite (no external service) - Trial balance: Query all entry lines, verify debits = credits - Account hierarchy: `:` separator parsed in TypeScript, permission inheritance via string prefix matching - Cost basis tracking: Store fiat metadata in entry line fields, never convert from current sats rate ### Summary Castle benefits from the most NIPs because accounting touches identity (NIP-58), communication (NIP-17/59), payments (NIP-47/57), data storage (NIP-78), classification (NIP-32), and access control (NIP-51). The protocol provides every primitive needed — the extension's job is to enforce double-entry invariants and orchestrate the workflow.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: aiolabs/lightning-pub#12
No description provided.