Date: 2025-01-12 Prepared By: Senior Accounting Review Subject: Castle Extension - Lightning Payment Settlement Entries Status: Technical Review
This document provides a professional accounting assessment of Castle’s net settlement entry pattern used for recording Lightning Network payments that settle fiat-denominated receivables. The analysis identifies areas where the implementation deviates from traditional accounting best practices and provides specific recommendations for improvement.
Key Findings: - ✅ Double-entry integrity maintained - ✅ Functional for intended purpose - ❌ Zero-amount postings violate accounting principles - ❌ Redundant satoshi tracking - ❌ No exchange gain/loss recognition - ⚠️ Mixed currency approach lacks clear hierarchy
Castle operates as a Lightning Network-integrated accounting system for collectives (co-living spaces, makerspaces). It faces a unique accounting challenge:
Scenario: User creates a receivable in EUR (e.g., €200 for room rent), then pays via Lightning Network in satoshis (225,033 sats).
Challenge: Record the payment while: 1. Clearing the exact EUR receivable amount 2. Recording the exact satoshi amount received 3. Handling cases where users have both receivables (owe Castle) and payables (Castle owes them) 4. Maintaining Beancount double-entry balance
; Step 1: Receivable Created
2025-11-12 * "room (200.00 EUR)" #receivable-entry
user-id: "375ec158"
source: "castle-api"
sats-amount: "225033"
Assets:Receivable:User-375ec158 200.00 EUR
sats-equivalent: "225033"
Income:Accommodation:Guests -200.00 EUR
sats-equivalent: "225033"
; Step 2: Lightning Payment Received
2025-11-12 * "Lightning payment settlement from user 375ec158"
#lightning-payment #net-settlement
user-id: "375ec158"
source: "lightning_payment"
payment-type: "net-settlement"
payment-hash: "8d080ec4cc4301715535004156085dd50c159185..."
Assets:Bitcoin:Lightning 225033 SATS @ 0.0008887585... EUR
payment-hash: "8d080ec4cc4301715535004156085dd50c159185..."
Assets:Receivable:User-375ec158 -200.00 EUR
sats-equivalent: "225033"
Liabilities:Payable:User-375ec158 0.00 EUR
Location:
beancount_format.py:739-760
# Build postings for net settlement
postings = [
{
"account": payment_account,
"amount": f"{abs(amount_sats)} SATS @@ {abs(net_fiat_amount):.2f} {fiat_currency}",
"meta": {"payment-hash": payment_hash} if payment_hash else {}
},
{
"account": receivable_account,
"amount": f"-{abs(total_receivable_fiat):.2f} {fiat_currency}",
"meta": {"sats-equivalent": str(abs(amount_sats))}
},
{
"account": payable_account,
"amount": f"{abs(total_payable_fiat):.2f} {fiat_currency}",
"meta": {}
}
]Three-Posting Structure: 1. Lightning
Account: Records SATS received with @@ total price
notation 2. Receivable Account: Clears EUR receivable
with sats-equivalent metadata 3. Payable Account:
Clears any outstanding EUR payables (often 0.00)
Problem: The third posting often records
0.00 EUR when no payable exists.
Liabilities:Payable:User-375ec158 0.00 EUR
Why This Is Wrong: - Zero-amount postings have no economic substance - Clutters the journal with non-events - Violates the principle of materiality (GAAP Concept Statement 2) - Makes auditing more difficult (reviewers must verify why zero amounts exist)
Accounting Principle Violated: > “Transactions should only include postings that represent actual economic events or changes in account balances.”
Impact: Low severity, but unprofessional presentation
Recommendation:
# Make payable posting conditional
postings = [
{"account": payment_account, "amount": ...},
{"account": receivable_account, "amount": ...}
]
# Only add payable posting if there's actually a payable
if total_payable_fiat > 0:
postings.append({
"account": payable_account,
"amount": f"{abs(total_payable_fiat):.2f} {fiat_currency}",
"meta": {}
})Problem: Satoshis are tracked in TWO places in the same transaction:
Position Amount (via @@
notation):
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EURMetadata (sats-equivalent):
Assets:Receivable:User-375ec158 -200.00 EUR
sats-equivalent: "225033"Why This Is Problematic: - The @@
notation already records the exact satoshi amount - Beancount’s price
database stores this relationship - Metadata becomes redundant for this
specific posting - Increases storage and potential for inconsistency
Technical Detail:
The @@ notation means “total price” and Beancount
converts it to per-unit price:
; You write:
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
; Beancount stores:
Assets:Bitcoin:Lightning 225033 SATS @ 0.0008887585... EUR
; (where 200.00 / 225033 = 0.0008887585...)
Beancount can query this:
SELECT account, sum(convert(position, SATS))
WHERE account = 'Assets:Bitcoin:Lightning'Recommendation:
Choose ONE approach consistently:
Option A - Use @ notation (Beancount standard):
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
payment-hash: "8d080ec4..."
Assets:Receivable:User-375ec158 -200.00 EUR
; No sats-equivalent needed here
Option B - Use EUR positions with metadata (Castle’s current approach):
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: "225033"
payment-hash: "8d080ec4..."
Assets:Receivable:User-375ec158 -200.00 EUR
sats-cleared: "225033"
Don’t: Mix both in the same transaction (current implementation)
Problem: When receivables are denominated in one currency (EUR) and paid in another (SATS), exchange rate fluctuations create gains or losses that should be recognized.
Example Scenario:
Day 1 - Receivable Created:
200 EUR = 225,033 SATS (rate: 1,125.165 sats/EUR)
Day 5 - Payment Received:
225,033 SATS = 199.50 EUR (rate: 1,127.682 sats/EUR)
Exchange rate moved unfavorably
Economic Reality: 0.50 EUR LOSS
Current Implementation: Forces balance by
calculating the @ rate to make it exactly 200 EUR:
Assets:Bitcoin:Lightning 225033 SATS @ 0.000888... EUR ; = exactly 200.00 EUR
This hides the exchange variance by treating the payment as if it was worth exactly the receivable amount.
GAAP/IFRS Requirement:
Under both US GAAP (ASC 830) and IFRS (IAS 21), exchange gains and losses on monetary items (like receivables) should be recognized in the period they occur.
Proper Accounting Treatment:
2025-11-12 * "Lightning payment with exchange loss"
Assets:Bitcoin:Lightning 225033 SATS @ 0.000886... EUR
; Market rate at payment time = 199.50 EUR
Expenses:Foreign-Exchange-Loss 0.50 EUR
Assets:Receivable:User-375ec158 -200.00 EUR
Impact: Moderate severity - affects financial statement accuracy
Why This Matters: - Tax reporting may require exchange gain/loss recognition - Financial statements misstate true economic results - Auditors would flag this as a compliance issue - Cannot accurately calculate ROI or performance metrics
Problem: The @ notation in Beancount
represents acquisition cost, not settlement
value.
Current Usage:
Assets:Bitcoin:Lightning 225033 SATS @ 0.000888... EUR
What this notation means in accounting: “We purchased 225,033 satoshis at a cost of 0.000888 EUR per satoshi”
What actually happened: “We received 225,033 satoshis as payment for a debt”
Economic Difference: - Purchase: You exchange cash for an asset (buying Bitcoin) - Payment Receipt: You receive an asset in settlement of a receivable
Accounting Substance vs. Form: - Form: The transaction looks like a Bitcoin purchase - Substance: The transaction is actually a receivable collection
GAAP Principle (ASC 105-10-05): > “Accounting should reflect the economic substance of transactions, not merely their legal form.”
Why This Creates Issues:
Proper Accounting Approach:
; Approach 1: Record at fair market value
Assets:Bitcoin:Lightning 225033 SATS @ 0.000886... EUR
; Using actual market price at time of receipt
acquisition-type: "payment-received"
Revenue:Exchange-Gain 0.50 EUR
Assets:Receivable:User-375ec158 -200.00 EUR
; Approach 2: Don't use @ notation at all
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: "225033"
fmv-at-receipt: "199.50 EUR"
Assets:Receivable:User-375ec158 -200.00 EUR
Problem: Function is called
format_net_settlement_entry, but it’s used for simple
payments that aren’t true net settlements.
Example from User’s Transaction: - Receivable: 200.00 EUR - Payable: 0.00 EUR - Net: 200.00 EUR (this is just a payment, not a settlement)
Accounting Terminology:
When Net Settlement is Appropriate:
User owes Castle: 555.00 EUR (receivable)
Castle owes User: 38.00 EUR (payable)
Net amount due: 517.00 EUR (true settlement)
Proper three-posting entry:
Assets:Bitcoin:Lightning 565251 SATS @@ 517.00 EUR
Assets:Receivable:User -555.00 EUR
Liabilities:Payable:User 38.00 EUR
; Net: 517.00 = -555.00 + 38.00 ✓
When Two Postings Suffice:
User owes Castle: 200.00 EUR (receivable)
Castle owes User: 0.00 EUR (no payable)
Amount due: 200.00 EUR (simple payment)
Simpler two-posting entry:
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
Assets:Receivable:User -200.00 EUR
Best Practice: Use the simplest journal entry structure that accurately represents the transaction.
Recommendation: 1. Rename function to
format_payment_entry or
format_receivable_payment_entry 2. Create separate
format_net_settlement_entry for true netting scenarios 3.
Use conditional logic to choose 2-posting vs 3-posting based on whether
both receivables AND payables exist
2025-11-12 * "Bitcoin payment from user 375ec158"
Assets:Bitcoin:Lightning 199.50 EUR
sats-received: "225033"
fmv-per-sat: "0.000886 EUR"
cost-basis: "199.50 EUR"
payment-hash: "8d080ec4..."
Revenue:Exchange-Gain 0.50 EUR
source: "cryptocurrency-receipt"
Assets:Receivable:User-375ec158 -200.00 EUR
Pros: - ✅ Tax compliant (establishes cost basis) - ✅ Recognizes exchange gain/loss - ✅ Uses actual market rates - ✅ Audit trail for cryptocurrency receipts
Cons: - ❌ Requires real-time price feeds - ❌ Creates taxable events
2025-11-12 * "Bitcoin payment from user 375ec158"
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: "225033"
sats-rate: "1125.165"
payment-hash: "8d080ec4..."
Assets:Receivable:User-375ec158 -200.00 EUR
Pros: - ✅ Simple and clean - ✅ EUR positions match accounting reality - ✅ SATS tracked in metadata for reference - ✅ No artificial price notation
Cons: - ❌ SATS not queryable via Beancount positions - ❌ Requires metadata parsing for SATS balances
2025-11-12 * "Net settlement via Lightning"
; User owes 555 EUR, Castle owes 38 EUR, net: 517 EUR
Assets:Bitcoin:Lightning 517.00 EUR
sats-received: "565251"
Assets:Receivable:User-375ec158 -555.00 EUR
Liabilities:Payable:User-375ec158 38.00 EUR
When to Use: Only when both receivables and payables exist and you’re truly netting them.
File: beancount_format.py:739-760
Current Code:
postings = [
{...}, # Lightning
{...}, # Receivable
{ # Payable (always included, even if 0.00)
"account": payable_account,
"amount": f"{abs(total_payable_fiat):.2f} {fiat_currency}",
"meta": {}
}
]Fixed Code:
postings = [
{
"account": payment_account,
"amount": f"{abs(amount_sats)} SATS @@ {abs(net_fiat_amount):.2f} {fiat_currency}",
"meta": {"payment-hash": payment_hash} if payment_hash else {}
},
{
"account": receivable_account,
"amount": f"-{abs(total_receivable_fiat):.2f} {fiat_currency}",
"meta": {"sats-equivalent": str(abs(amount_sats))}
}
]
# Only add payable posting if there's actually a payable to clear
if total_payable_fiat > 0:
postings.append({
"account": payable_account,
"amount": f"{abs(total_payable_fiat):.2f} {fiat_currency}",
"meta": {}
})Impact: Cleaner journal, professional presentation, easier auditing
Decision Required: Select either position-based OR metadata-based satoshi tracking.
Option A - Keep Metadata Approach (recommended for Castle):
# In format_net_settlement_entry()
postings = [
{
"account": payment_account,
"amount": f"{abs(net_fiat_amount):.2f} {fiat_currency}", # EUR only
"meta": {
"sats-received": str(abs(amount_sats)),
"payment-hash": payment_hash
}
},
{
"account": receivable_account,
"amount": f"-{abs(total_receivable_fiat):.2f} {fiat_currency}",
"meta": {"sats-cleared": str(abs(amount_sats))}
}
]Option B - Use Position-Based Tracking:
# Remove sats-equivalent metadata entirely
postings = [
{
"account": payment_account,
"amount": f"{abs(amount_sats)} SATS @@ {abs(net_fiat_amount):.2f} {fiat_currency}",
"meta": {"payment-hash": payment_hash}
},
{
"account": receivable_account,
"amount": f"-{abs(total_receivable_fiat):.2f} {fiat_currency}",
# No sats-equivalent needed - queryable via price database
}
]Recommendation: Choose Option A (metadata) for consistency with Castle’s architecture.
File: beancount_format.py
Current:
format_net_settlement_entry()
New: format_receivable_payment_entry()
or format_payment_settlement_entry()
Rationale: More accurately describes what the function does (processes payments, not always net settlements)
File: tasks.py:259-276 (get balance and
calculate settlement)
New Logic:
# Get user's current balance
balance = await fava.get_user_balance(user_id)
fiat_balances = balance.get("fiat_balances", {})
total_fiat_balance = fiat_balances.get(fiat_currency, Decimal(0))
# Calculate expected fiat value of SATS payment at current market rate
market_rate = await get_current_sats_eur_rate() # New function needed
market_value = Decimal(amount_sats) * market_rate
# Calculate exchange variance
receivable_amount = abs(total_fiat_balance) if total_fiat_balance > 0 else Decimal(0)
exchange_variance = market_value - receivable_amount
# If variance is material (> 1 cent), create exchange gain/loss posting
if abs(exchange_variance) > Decimal("0.01"):
# Add exchange gain/loss to postings
if exchange_variance > 0:
# Gain: payment worth more than receivable
exchange_account = "Revenue:Foreign-Exchange-Gain"
else:
# Loss: payment worth less than receivable
exchange_account = "Expenses:Foreign-Exchange-Loss"
# Include in entry creation
exchange_posting = {
"account": exchange_account,
"amount": f"{abs(exchange_variance):.2f} {fiat_currency}",
"meta": {
"sats-amount": str(amount_sats),
"market-rate": str(market_rate),
"receivable-amount": str(receivable_amount)
}
}Benefits: - ✅ Tax compliance - ✅ Accurate financial reporting - ✅ Audit trail for cryptocurrency gains/losses - ✅ Regulatory compliance (GAAP/IFRS)
File: tasks.py or new
payment_logic.py
async def create_payment_entry(
user_id: str,
amount_sats: int,
fiat_amount: Decimal,
fiat_currency: str,
payment_hash: str
):
"""
Create appropriate payment entry based on user's balance situation.
Uses 2-posting for simple payments, 3-posting for net settlements.
"""
# Get user balance
balance = await fava.get_user_balance(user_id)
fiat_balances = balance.get("fiat_balances", {})
total_balance = fiat_balances.get(fiat_currency, Decimal(0))
receivable_amount = Decimal(0)
payable_amount = Decimal(0)
if total_balance > 0:
receivable_amount = total_balance
elif total_balance < 0:
payable_amount = abs(total_balance)
# Determine entry type
if receivable_amount > 0 and payable_amount > 0:
# TRUE NET SETTLEMENT: Both obligations exist
return await format_net_settlement_entry(
user_id=user_id,
amount_sats=amount_sats,
receivable_amount=receivable_amount,
payable_amount=payable_amount,
fiat_amount=fiat_amount,
fiat_currency=fiat_currency,
payment_hash=payment_hash
)
elif receivable_amount > 0:
# SIMPLE RECEIVABLE PAYMENT: Only receivable exists
return await format_receivable_payment_entry(
user_id=user_id,
amount_sats=amount_sats,
receivable_amount=receivable_amount,
fiat_amount=fiat_amount,
fiat_currency=fiat_currency,
payment_hash=payment_hash
)
else:
# PAYABLE PAYMENT: Castle paying user (different flow)
return await format_payable_payment_entry(...)Current Issue: Mixed approach (EUR positions with SATS metadata, but also SATS positions with @ notation)
Decision Required: Choose ONE of the following architectures:
Architecture A - EUR Primary, SATS Secondary (recommended):
; All positions in EUR, SATS in metadata
2025-11-12 * "Payment"
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: "225033"
Assets:Receivable:User -200.00 EUR
sats-cleared: "225033"
Architecture B - SATS Primary, EUR Secondary:
; All positions in SATS, EUR in metadata
2025-11-12 * "Payment"
Assets:Bitcoin:Lightning 225033 SATS
eur-value: "200.00"
Assets:Receivable:User -225033 SATS
eur-cleared: "200.00"
Recommendation: Architecture A (EUR primary) because: 1. Most receivables created in EUR 2. Financial reporting requirements typically in fiat 3. Tax obligations calculated in fiat 4. Aligns with current Castle metadata approach
Advanced Approach: Separate cryptocurrency movements from fiat accounting
Main Ledger (EUR-denominated):
2025-11-12 * "Payment received from user"
Assets:Bitcoin-Custody:User-375ec158 200.00 EUR
Assets:Receivable:User-375ec158 -200.00 EUR
Cryptocurrency Sub-Ledger (SATS-denominated):
2025-11-12 * "Lightning payment received"
Assets:Bitcoin:Lightning:Castle 225033 SATS
Assets:Bitcoin:Custody:User-375ec 225033 SATS
Benefits: - ✅ Clean separation of concerns - ✅ Cryptocurrency movements tracked independently - ✅ Fiat accounting unaffected by Bitcoin volatility - ✅ Can generate separate financial statements
Drawbacks: - ❌ Increased complexity - ❌ Reconciliation between ledgers required - ❌ Two sets of books to maintain
beancount_format.py:739-760
beancount_format.py:692
format_receivable_payment_entrytasks.py:235-310
exchange_rates.py
get_current_sats_eur_rate() functionbeancount_format.py
format_net_settlement_entry() for true
nettingformat_receivable_payment_entry() for simple
paymentsSetup: - User has receivable: 200.00 EUR - User has payable: 0.00 EUR - User pays: 225,033 SATS
Expected Entry (after fixes):
2025-11-12 * "Lightning payment from user"
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: "225033"
payment-hash: "8d080ec4..."
Assets:Receivable:User -200.00 EUR
sats-cleared: "225033"
Verify: - ✅ Only 2 postings (no zero-amount payable) - ✅ Entry balances - ✅ SATS tracked in metadata - ✅ User balance becomes 0 (both EUR and SATS)
Setup: - User has receivable: 555.00 EUR - User has payable: 38.00 EUR - Net owed: 517.00 EUR - User pays: 565,251 SATS (worth 517.00 EUR)
Expected Entry:
2025-11-12 * "Net settlement via Lightning"
Assets:Bitcoin:Lightning 517.00 EUR
sats-received: "565251"
payment-hash: "abc123..."
Assets:Receivable:User -555.00 EUR
sats-portion: "565251"
Liabilities:Payable:User 38.00 EUR
Verify: - ✅ 3 postings (receivable + payable cleared) - ✅ Net amount = receivable - payable - ✅ Both balances become 0 - ✅ Mathematically balanced
Setup: - User has receivable: 200.00 EUR (created at 1,125 sats/EUR) - User pays: 225,033 SATS (now worth 199.50 EUR at market) - Exchange loss: 0.50 EUR
Expected Entry (with exchange tracking):
2025-11-12 * "Lightning payment with exchange loss"
Assets:Bitcoin:Lightning 199.50 EUR
sats-received: "225033"
market-rate: "0.000886"
Expenses:Foreign-Exchange-Loss 0.50 EUR
Assets:Receivable:User -200.00 EUR
Verify: - ✅ Bitcoin recorded at fair market value - ✅ Exchange loss recognized - ✅ Receivable cleared at book value - ✅ Entry balances
| Issue | Severity | Accounting Impact | Recommended Action |
|---|---|---|---|
| Zero-amount postings | Low | Presentation only | Remove immediately |
| Redundant SATS tracking | Low | Storage/efficiency | Choose one method |
| No exchange gain/loss | High | Financial accuracy | Implement for compliance |
| Semantic misuse of @ | Medium | Audit clarity | Consider EUR-only positions |
| Misnamed function | Low | Code clarity | Rename function |
Is this “best practice” accounting? No, this implementation deviates from traditional accounting standards in several ways.
Is it acceptable for Castle’s use case? Yes, with modifications, it’s a reasonable pragmatic solution for a novel problem (cryptocurrency payments of fiat debts).
Critical improvements needed: 1. ✅ Remove zero-amount postings (easy fix, professional presentation) 2. ✅ Implement exchange gain/loss tracking (required for compliance) 3. ✅ Separate payment vs. settlement logic (accuracy and clarity)
The fundamental challenge: Traditional accounting wasn’t designed for this scenario. There is no established “standard” for recording cryptocurrency payments of fiat-denominated receivables. Castle’s approach is functional, but should be refined to align better with accounting principles where possible.
docs/SATS-EQUIVALENT-METADATA.mddocs/BQL-BALANCE-QUERIES.mdDocument Version: 1.0 Last Updated: 2025-01-12 Next Review: After Priority 1 fixes implemented
This analysis was prepared for internal review and development planning. It represents a professional accounting assessment of the current implementation and should be used to guide improvements to Castle’s payment recording system.