Creates a markdown file to provide guidance to Claude Code (claude.ai/code) when interacting with the Castle Accounting codebase. This document outlines project overview, architecture, key files, database schema, transaction flows, API endpoints, development notes, and other crucial information. It aims to improve Claude's ability to understand and assist with code-related tasks.
11 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Castle Accounting is a double-entry bookkeeping extension for LNbits that enables collectives (co-living spaces, makerspaces, community projects) to track finances with proper accounting principles. It integrates Lightning Network payments with traditional accounting, supporting both cryptocurrency and fiat currency tracking.
Architecture
Core Design Principles
Double-Entry Accounting: Every transaction affects at least two accounts. Debits must equal credits. Five account types: Assets, Liabilities, Equity, Revenue (Income), Expenses.
Pure Functional Core: The core/ directory contains pure accounting logic independent of the database layer:
core/balance.py- Balance calculation from journal entriescore/inventory.py- Multi-currency position tracking (similar to Beancount's Inventory)core/validation.py- Entry validation rules
Account Hierarchy: Beancount-style hierarchical naming with : separators:
Assets:Lightning:BalanceAssets:Receivable:User-af983632Liabilities:Payable:User-af983632Expenses:Food:Supplies
Metadata System: Each entry_line stores JSON metadata preserving original fiat amounts. Critical: fiat balances are calculated by summing fiat_amount from metadata, NOT by converting current satoshi balances. This prevents exchange rate fluctuations from affecting historical records.
Key Files
models.py- Pydantic models for API I/O and data structurescrud.py- Database operations (create/read/update accounts, journal entries)views_api.py- FastAPI endpoints for all operationsviews.py- Web interface routingservices.py- Settings management layermigrations.py- Database schema migrationstasks.py- Background tasks (daily reconciliation checks)account_utils.py- Hierarchical account naming utilities
Database Schema
accounts: Chart of accounts with hierarchical names
user_idfield for per-user accounts (Receivable, Payable, Equity)- Indexed on
user_idandaccount_type
journal_entries: Transaction headers
flagfield:*(cleared),!(pending),#(flagged),x(void)metafield: JSON storing source, tags, audit inforeferencefield: Links to payment_hash, invoice numbers, etc.
entry_lines: Individual debit/credit lines
- Always balanced (sum of debits = sum of credits per entry)
metadatafield stores fiat currency info as JSON- Indexed on
journal_entry_idandaccount_id
balance_assertions: Reconciliation checkpoints (Beancount-style)
- Assert expected balance at a date
- Status: pending, passed, failed
- Used for daily reconciliation checks
extension_settings: Castle wallet configuration (admin-only)
user_wallet_settings: Per-user wallet configuration
manual_payment_requests: User requests for cash/manual payments
Transaction Flows
User Adds Expense (Liability)
User pays cash for groceries, Castle owes them:
DR Expenses:Food 39,669 sats
CR Liabilities:Payable:User-af983632 39,669 sats
Metadata preserves: {"fiat_currency": "EUR", "fiat_amount": "36.93", "fiat_rate": "1074.192"}
Castle Adds Receivable
User owes Castle for accommodation:
DR Assets:Receivable:User-af983632 268,548 sats
CR Income:Accommodation 268,548 sats
User Pays with Lightning
Invoice generated on Castle's wallet (not user's). After payment:
DR Assets:Lightning:Balance 268,548 sats
CR Assets:Receivable:User-af983632 268,548 sats
Manual Payment Approval
User requests cash payment → Admin approves → Journal entry created:
DR Liabilities:Payable:User-af983632 39,669 sats
CR Assets:Lightning:Balance 39,669 sats
Balance Calculation Logic
User Balance:
- Positive = Castle owes user (LIABILITY accounts have credit balance)
- Negative = User owes Castle (ASSET accounts have debit balance)
- Calculated from sum of all entry lines across user's accounts
- Fiat balances summed from metadata, NOT converted from sats
Perspective-Based UI:
- User View: Green = Castle owes them, Red = They owe Castle
- Castle Admin View: Green = User owes Castle, Red = Castle owes user
API Endpoints
Accounts
GET /api/v1/accounts- List all accountsPOST /api/v1/accounts- Create account (admin)GET /api/v1/accounts/{id}/balance- Get account balance
Journal Entries
POST /api/v1/entries/expense- User adds expense (creates liability or equity)POST /api/v1/entries/receivable- Admin records what user owes (admin only)POST /api/v1/entries/revenue- Admin records direct revenue (admin only)GET /api/v1/entries/user- Get user's journal entriesPOST /api/v1/entries- Create raw journal entry (admin only)
Payments & Balances
GET /api/v1/balance- Get user balance (or Castle total if super user)GET /api/v1/balances/all- Get all user balances (admin, enriched with usernames)POST /api/v1/generate-payment-invoice- Generate invoice for user to pay CastlePOST /api/v1/record-payment- Record Lightning payment from user to CastlePOST /api/v1/settle-receivable- Manually settle receivable (cash/bank)POST /api/v1/pay-user- Castle pays user (cash/bank/lightning)
Manual Payment Requests
POST /api/v1/manual-payment-requests- User requests paymentGET /api/v1/manual-payment-requests- User's requestsGET /api/v1/manual-payment-requests/all- All requests (admin)POST /api/v1/manual-payment-requests/{id}/approve- Approve (admin)POST /api/v1/manual-payment-requests/{id}/reject- Reject (admin)
Reconciliation
POST /api/v1/assertions/balance- Create balance assertionGET /api/v1/assertions/balance- List balance assertionsPOST /api/v1/assertions/balance/{id}/check- Check assertionPOST /api/v1/tasks/daily-reconciliation- Run daily reconciliation (admin)
Settings
GET /api/v1/settings- Get Castle settings (super user)PUT /api/v1/settings- Update Castle settings (super user)GET /api/v1/user/wallet- Get user wallet settingsPUT /api/v1/user/wallet- Update user wallet settings
Development Notes
Testing Entry Creation
When creating journal entries programmatically, use the helper endpoints:
POST /api/v1/entries/expensefor user expenses (handles account creation automatically)POST /api/v1/entries/receivablefor what users owePOST /api/v1/entries/revenuefor direct revenue
For custom entries, use POST /api/v1/entries with properly balanced lines.
User Account Management
User-specific accounts are created automatically with format:
- Assets:
Assets:Receivable:User-{user_id[:8]} - Liabilities:
Liabilities:Payable:User-{user_id[:8]} - Equity:
Equity:MemberEquity:User-{user_id[:8]}
Use get_or_create_user_account() in crud.py to ensure consistency.
Currency Handling
CRITICAL: Use Decimal for all fiat amounts, never float. Fiat amounts are stored in metadata as strings to preserve precision:
from decimal import Decimal
metadata = {
"fiat_currency": "EUR",
"fiat_amount": str(Decimal("250.00")),
"fiat_rate": str(Decimal("1074.192")),
"btc_rate": str(Decimal("0.000931"))
}
When reading: fiat_amount = Decimal(metadata["fiat_amount"])
Balance Assertions for Reconciliation
Create balance assertions to verify accounting accuracy:
await create_balance_assertion(
account_id="lightning_account_id",
expected_balance_sats=1000000,
expected_balance_fiat=Decimal("500.00"),
fiat_currency="EUR",
tolerance_sats=100
)
Run POST /api/v1/tasks/daily-reconciliation to check all assertions.
Permission Model
- Super User: Full access (check via
wallet.wallet.user == lnbits_settings.super_user) - Admin Key: Required for creating receivables, approving payments, viewing all balances
- Invoice Key: Read access to user's own data
- Users: Can only see/manage their own accounts and transactions
Extension as LNbits Module
This extension follows LNbits extension structure:
- Registered via
castle_extrouter in__init__.py - Static files served from
static/directory - Templates in
templates/castle/ - Database accessed via
db = Database("ext_castle")
Common Tasks
Add New Expense Account
await create_account(CreateAccount(
name="Expenses:Internet",
account_type=AccountType.EXPENSE,
description="Internet service costs"
))
Manually Record Cash Payment
await create_journal_entry(CreateJournalEntry(
description="Cash payment for groceries",
lines=[
CreateEntryLine(account_id=expense_account_id, debit=50000),
CreateEntryLine(account_id=cash_account_id, credit=50000)
],
flag=JournalEntryFlag.CLEARED,
meta={"source": "manual", "payment_method": "cash"}
))
Check User Balance
balance = await get_user_balance(user_id)
print(f"Sats: {balance.balance}") # Positive = Castle owes user
print(f"Fiat: {balance.fiat_balances}") # {"EUR": Decimal("36.93")}
Export to Beancount (Future)
Follow patterns in docs/BEANCOUNT_PATTERNS.md for implementing Beancount export. Use hierarchical account names and preserve metadata in Beancount comments.
Data Integrity
Critical Invariants:
- Every journal entry MUST have balanced debits and credits
- Fiat balances calculated from metadata, not from converting sats
- User accounts use
user_id(NOTwallet_id) for consistency - Balance assertions checked daily via background task
Validation is performed in core/validation.py:
validate_journal_entry()- Checks balance, minimum linesvalidate_balance()- Verifies account balance calculationvalidate_receivable_entry()- Ensures receivable entries are validvalidate_expense_entry()- Ensures expense entries are valid
Known Issues & Future Work
See docs/DOCUMENTATION.md for comprehensive list. Key items:
- No journal entry editing/deletion (use reversing entries)
- No date range filtering on list endpoints (hardcoded limit of 100)
- No batch operations for bulk imports
- Plugin system architecture designed but not implemented
- Beancount export endpoint not yet implemented
Related Documentation
docs/README.md- User-facing overviewdocs/DOCUMENTATION.md- Comprehensive technical documentationdocs/BEANCOUNT_PATTERNS.md- Beancount-inspired design patternsdocs/PHASE1_COMPLETE.md,PHASE2_COMPLETE.md,PHASE3_COMPLETE.md- Development milestonesdocs/EXPENSE_APPROVAL.md- Manual payment request workflowdocs/DAILY_RECONCILIATION.md- Automated reconciliation system