This commit is contained in:
Padreug 2025-12-14 12:47:34 +01:00
parent 1d2eb05c36
commit 862fe0bfad
4 changed files with 2729 additions and 0 deletions

View file

@ -0,0 +1,953 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<meta charset="utf-8" />
<meta name="generator" content="pandoc" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<title>ACCOUNTING-ANALYSIS-NET-SETTLEMENT</title>
<style>
code{white-space: pre-wrap;}
span.smallcaps{font-variant: small-caps;}
div.columns{display: flex; gap: min(4vw, 1.5em);}
div.column{flex: auto; overflow-x: auto;}
div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;}
/* The extra [class] is a hack that increases specificity enough to
override a similar rule in reveal.js */
ul.task-list[class]{list-style: none;}
ul.task-list li input[type="checkbox"] {
font-size: inherit;
width: 0.8em;
margin: 0 0.8em 0.2em -1.6em;
vertical-align: middle;
}
.display.math{display: block; text-align: center; margin: 0.5rem auto;}
/* CSS for syntax highlighting */
html { -webkit-text-size-adjust: 100%; }
pre > code.sourceCode { white-space: pre; position: relative; }
pre > code.sourceCode > span { display: inline-block; line-height: 1.25; }
pre > code.sourceCode > span:empty { height: 1.2em; }
.sourceCode { overflow: visible; }
code.sourceCode > span { color: inherit; text-decoration: inherit; }
div.sourceCode { margin: 1em 0; }
pre.sourceCode { margin: 0; }
@media screen {
div.sourceCode { overflow: auto; }
}
@media print {
pre > code.sourceCode { white-space: pre-wrap; }
pre > code.sourceCode > span { text-indent: -5em; padding-left: 5em; }
}
pre.numberSource code
{ counter-reset: source-line 0; }
pre.numberSource code > span
{ position: relative; left: -4em; counter-increment: source-line; }
pre.numberSource code > span > a:first-child::before
{ content: counter(source-line);
position: relative; left: -1em; text-align: right; vertical-align: baseline;
border: none; display: inline-block;
-webkit-touch-callout: none; -webkit-user-select: none;
-khtml-user-select: none; -moz-user-select: none;
-ms-user-select: none; user-select: none;
padding: 0 4px; width: 4em;
color: #aaaaaa;
}
pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
div.sourceCode
{ }
@media screen {
pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; }
}
code span.al { color: #ff0000; font-weight: bold; } /* Alert */
code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
code span.at { color: #7d9029; } /* Attribute */
code span.bn { color: #40a070; } /* BaseN */
code span.bu { color: #008000; } /* BuiltIn */
code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
code span.ch { color: #4070a0; } /* Char */
code span.cn { color: #880000; } /* Constant */
code span.co { color: #60a0b0; font-style: italic; } /* Comment */
code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
code span.do { color: #ba2121; font-style: italic; } /* Documentation */
code span.dt { color: #902000; } /* DataType */
code span.dv { color: #40a070; } /* DecVal */
code span.er { color: #ff0000; font-weight: bold; } /* Error */
code span.ex { } /* Extension */
code span.fl { color: #40a070; } /* Float */
code span.fu { color: #06287e; } /* Function */
code span.im { color: #008000; font-weight: bold; } /* Import */
code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
code span.kw { color: #007020; font-weight: bold; } /* Keyword */
code span.op { color: #666666; } /* Operator */
code span.ot { color: #007020; } /* Other */
code span.pp { color: #bc7a00; } /* Preprocessor */
code span.sc { color: #4070a0; } /* SpecialChar */
code span.ss { color: #bb6688; } /* SpecialString */
code span.st { color: #4070a0; } /* String */
code span.va { color: #19177c; } /* Variable */
code span.vs { color: #4070a0; } /* VerbatimString */
code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
</style>
<link rel="stylesheet" href="https://latex.now.sh/style.css" />
</head>
<body>
<nav id="TOC" role="doc-toc">
<ul>
<li><a href="#accounting-analysis-net-settlement-entry-pattern"
id="toc-accounting-analysis-net-settlement-entry-pattern">Accounting
Analysis: Net Settlement Entry Pattern</a>
<ul>
<li><a href="#executive-summary" id="toc-executive-summary">Executive
Summary</a></li>
<li><a href="#background-the-technical-challenge"
id="toc-background-the-technical-challenge">Background: The Technical
Challenge</a></li>
<li><a href="#current-implementation"
id="toc-current-implementation">Current Implementation</a>
<ul>
<li><a href="#transaction-example"
id="toc-transaction-example">Transaction Example</a></li>
<li><a href="#code-implementation" id="toc-code-implementation">Code
Implementation</a></li>
</ul></li>
<li><a href="#accounting-issues-identified"
id="toc-accounting-issues-identified">Accounting Issues Identified</a>
<ul>
<li><a href="#issue-1-zero-amount-postings"
id="toc-issue-1-zero-amount-postings">Issue 1: Zero-Amount
Postings</a></li>
<li><a href="#issue-2-redundant-satoshi-tracking"
id="toc-issue-2-redundant-satoshi-tracking">Issue 2: Redundant Satoshi
Tracking</a></li>
<li><a href="#issue-3-no-exchange-gainloss-recognition"
id="toc-issue-3-no-exchange-gainloss-recognition">Issue 3: No Exchange
Gain/Loss Recognition</a></li>
<li><a href="#issue-4-semantic-misuse-of-price-notation"
id="toc-issue-4-semantic-misuse-of-price-notation">Issue 4: Semantic
Misuse of Price Notation</a></li>
<li><a href="#issue-5-misnamed-function-and-incorrect-usage"
id="toc-issue-5-misnamed-function-and-incorrect-usage">Issue 5: Misnamed
Function and Incorrect Usage</a></li>
</ul></li>
<li><a href="#traditional-accounting-approaches"
id="toc-traditional-accounting-approaches">Traditional Accounting
Approaches</a>
<ul>
<li><a
href="#approach-1-record-bitcoin-at-fair-market-value-tax-compliant"
id="toc-approach-1-record-bitcoin-at-fair-market-value-tax-compliant">Approach
1: Record Bitcoin at Fair Market Value (Tax Compliant)</a></li>
<li><a href="#approach-2-simplified-eur-only-ledger-no-sats-positions"
id="toc-approach-2-simplified-eur-only-ledger-no-sats-positions">Approach
2: Simplified EUR-Only Ledger (No SATS Positions)</a></li>
<li><a
href="#approach-3-true-net-settlement-when-both-obligations-exist"
id="toc-approach-3-true-net-settlement-when-both-obligations-exist">Approach
3: True Net Settlement (When Both Obligations Exist)</a></li>
</ul></li>
<li><a href="#recommendations"
id="toc-recommendations">Recommendations</a>
<ul>
<li><a href="#priority-1-immediate-fixes-easy-wins"
id="toc-priority-1-immediate-fixes-easy-wins">Priority 1: Immediate
Fixes (Easy Wins)</a></li>
<li><a href="#priority-2-medium-term-improvements-compliance"
id="toc-priority-2-medium-term-improvements-compliance">Priority 2:
Medium-Term Improvements (Compliance)</a></li>
<li><a href="#priority-3-long-term-architectural-decisions"
id="toc-priority-3-long-term-architectural-decisions">Priority 3:
Long-Term Architectural Decisions</a></li>
</ul></li>
<li><a href="#code-files-requiring-changes"
id="toc-code-files-requiring-changes">Code Files Requiring Changes</a>
<ul>
<li><a href="#high-priority-immediate-fixes"
id="toc-high-priority-immediate-fixes">High Priority (Immediate
Fixes)</a></li>
<li><a href="#medium-priority-compliance"
id="toc-medium-priority-compliance">Medium Priority
(Compliance)</a></li>
</ul></li>
<li><a href="#testing-requirements"
id="toc-testing-requirements">Testing Requirements</a>
<ul>
<li><a href="#test-case-1-simple-receivable-payment-no-payable"
id="toc-test-case-1-simple-receivable-payment-no-payable">Test Case 1:
Simple Receivable Payment (No Payable)</a></li>
<li><a href="#test-case-2-true-net-settlement"
id="toc-test-case-2-true-net-settlement">Test Case 2: True Net
Settlement</a></li>
<li><a href="#test-case-3-exchange-gainloss-future"
id="toc-test-case-3-exchange-gainloss-future">Test Case 3: Exchange
Gain/Loss (Future)</a></li>
</ul></li>
<li><a href="#conclusion" id="toc-conclusion">Conclusion</a>
<ul>
<li><a href="#summary-of-issues" id="toc-summary-of-issues">Summary of
Issues</a></li>
<li><a href="#professional-assessment"
id="toc-professional-assessment">Professional Assessment</a></li>
<li><a href="#next-steps" id="toc-next-steps">Next Steps</a></li>
</ul></li>
<li><a href="#references" id="toc-references">References</a></li>
</ul></li>
</ul>
</nav>
<h1 id="accounting-analysis-net-settlement-entry-pattern">Accounting
Analysis: Net Settlement Entry Pattern</h1>
<p><strong>Date</strong>: 2025-01-12 <strong>Prepared By</strong>:
Senior Accounting Review <strong>Subject</strong>: Castle Extension -
Lightning Payment Settlement Entries <strong>Status</strong>: Technical
Review</p>
<hr />
<h2 id="executive-summary">Executive Summary</h2>
<p>This document provides a professional accounting assessment of
Castles 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.</p>
<p><strong>Key Findings</strong>: - ✅ 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</p>
<hr />
<h2 id="background-the-technical-challenge">Background: The Technical
Challenge</h2>
<p>Castle operates as a Lightning Network-integrated accounting system
for collectives (co-living spaces, makerspaces). It faces a unique
accounting challenge:</p>
<p><strong>Scenario</strong>: User creates a receivable in EUR (e.g.,
€200 for room rent), then pays via Lightning Network in satoshis
(225,033 sats).</p>
<p><strong>Challenge</strong>: 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</p>
<hr />
<h2 id="current-implementation">Current Implementation</h2>
<h3 id="transaction-example">Transaction Example</h3>
<pre class="beancount"><code>; Step 1: Receivable Created
2025-11-12 * &quot;room (200.00 EUR)&quot; #receivable-entry
user-id: &quot;375ec158&quot;
source: &quot;castle-api&quot;
sats-amount: &quot;225033&quot;
Assets:Receivable:User-375ec158 200.00 EUR
sats-equivalent: &quot;225033&quot;
Income:Accommodation:Guests -200.00 EUR
sats-equivalent: &quot;225033&quot;
; Step 2: Lightning Payment Received
2025-11-12 * &quot;Lightning payment settlement from user 375ec158&quot;
#lightning-payment #net-settlement
user-id: &quot;375ec158&quot;
source: &quot;lightning_payment&quot;
payment-type: &quot;net-settlement&quot;
payment-hash: &quot;8d080ec4cc4301715535004156085dd50c159185...&quot;
Assets:Bitcoin:Lightning 225033 SATS @ 0.0008887585... EUR
payment-hash: &quot;8d080ec4cc4301715535004156085dd50c159185...&quot;
Assets:Receivable:User-375ec158 -200.00 EUR
sats-equivalent: &quot;225033&quot;
Liabilities:Payable:User-375ec158 0.00 EUR</code></pre>
<h3 id="code-implementation">Code Implementation</h3>
<p><strong>Location</strong>:
<code>beancount_format.py:739-760</code></p>
<div class="sourceCode" id="cb2"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Build postings for net settlement</span></span>
<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payment_account,</span>
<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(amount_sats)<span class="sc">}</span><span class="ss"> SATS @@ </span><span class="sc">{</span><span class="bu">abs</span>(net_fiat_amount)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;payment-hash&quot;</span>: payment_hash} <span class="cf">if</span> payment_hash <span class="cf">else</span> {}</span>
<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: receivable_account,</span>
<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;-</span><span class="sc">{</span><span class="bu">abs</span>(total_receivable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;sats-equivalent&quot;</span>: <span class="bu">str</span>(<span class="bu">abs</span>(amount_sats))}</span>
<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payable_account,</span>
<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(total_payable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {}</span>
<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a>]</span></code></pre></div>
<p><strong>Three-Posting Structure</strong>: 1. <strong>Lightning
Account</strong>: Records SATS received with <code>@@</code> total price
notation 2. <strong>Receivable Account</strong>: Clears EUR receivable
with sats-equivalent metadata 3. <strong>Payable Account</strong>:
Clears any outstanding EUR payables (often 0.00)</p>
<hr />
<h2 id="accounting-issues-identified">Accounting Issues Identified</h2>
<h3 id="issue-1-zero-amount-postings">Issue 1: Zero-Amount Postings</h3>
<p><strong>Problem</strong>: The third posting often records
<code>0.00 EUR</code> when no payable exists.</p>
<pre class="beancount"><code>Liabilities:Payable:User-375ec158 0.00 EUR</code></pre>
<p><strong>Why This Is Wrong</strong>: - 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)</p>
<p><strong>Accounting Principle Violated</strong>: &gt; “Transactions
should only include postings that represent actual economic events or
changes in account balances.”</p>
<p><strong>Impact</strong>: Low severity, but unprofessional
presentation</p>
<p><strong>Recommendation</strong>:</p>
<div class="sourceCode" id="cb4"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Make payable posting conditional</span></span>
<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a> {<span class="st">&quot;account&quot;</span>: payment_account, <span class="st">&quot;amount&quot;</span>: ...},</span>
<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a> {<span class="st">&quot;account&quot;</span>: receivable_account, <span class="st">&quot;amount&quot;</span>: ...}</span>
<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a>]</span>
<span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="co"># Only add payable posting if there&#39;s actually a payable</span></span>
<span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> total_payable_fiat <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a> postings.append({</span>
<span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payable_account,</span>
<span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(total_payable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {}</span>
<span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a> })</span></code></pre></div>
<hr />
<h3 id="issue-2-redundant-satoshi-tracking">Issue 2: Redundant Satoshi
Tracking</h3>
<p><strong>Problem</strong>: Satoshis are tracked in TWO places in the
same transaction:</p>
<ol type="1">
<li><p><strong>Position Amount</strong> (via <code>@@</code>
notation):</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR</code></pre></li>
<li><p><strong>Metadata</strong> (sats-equivalent):</p>
<pre class="beancount"><code>Assets:Receivable:User-375ec158 -200.00 EUR
sats-equivalent: &quot;225033&quot;</code></pre></li>
</ol>
<p><strong>Why This Is Problematic</strong>: - The <code>@@</code>
notation already records the exact satoshi amount - Beancounts price
database stores this relationship - Metadata becomes redundant for this
specific posting - Increases storage and potential for inconsistency</p>
<p><strong>Technical Detail</strong>:</p>
<p>The <code>@@</code> notation means “total price” and Beancount
converts it to per-unit price:</p>
<pre class="beancount"><code>; 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...)</code></pre>
<p>Beancount can query this:</p>
<div class="sourceCode" id="cb8"><pre
class="sourceCode sql"><code class="sourceCode sql"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="kw">SELECT</span> <span class="kw">account</span>, <span class="fu">sum</span>(<span class="fu">convert</span>(position, SATS))</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="kw">WHERE</span> <span class="kw">account</span> <span class="op">=</span> <span class="st">&#39;Assets:Bitcoin:Lightning&#39;</span></span></code></pre></div>
<p><strong>Recommendation</strong>:</p>
<p>Choose ONE approach consistently:</p>
<p><strong>Option A - Use @ notation</strong> (Beancount standard):</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
payment-hash: &quot;8d080ec4...&quot;
Assets:Receivable:User-375ec158 -200.00 EUR
; No sats-equivalent needed here</code></pre>
<p><strong>Option B - Use EUR positions with metadata</strong> (Castles
current approach):</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
payment-hash: &quot;8d080ec4...&quot;
Assets:Receivable:User-375ec158 -200.00 EUR
sats-cleared: &quot;225033&quot;</code></pre>
<p><strong>Dont</strong>: Mix both in the same transaction (current
implementation)</p>
<hr />
<h3 id="issue-3-no-exchange-gainloss-recognition">Issue 3: No Exchange
Gain/Loss Recognition</h3>
<p><strong>Problem</strong>: When receivables are denominated in one
currency (EUR) and paid in another (SATS), exchange rate fluctuations
create gains or losses that should be recognized.</p>
<p><strong>Example Scenario</strong>:</p>
<pre><code>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</code></pre>
<p><strong>Current Implementation</strong>: Forces balance by
calculating the <code>@</code> rate to make it exactly 200 EUR:</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @ 0.000888... EUR ; = exactly 200.00 EUR</code></pre>
<p>This <strong>hides the exchange variance</strong> by treating the
payment as if it was worth exactly the receivable amount.</p>
<p><strong>GAAP/IFRS Requirement</strong>:</p>
<p>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.</p>
<p><strong>Proper Accounting Treatment</strong>:</p>
<pre class="beancount"><code>2025-11-12 * &quot;Lightning payment with exchange loss&quot;
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</code></pre>
<p><strong>Impact</strong>: Moderate severity - affects financial
statement accuracy</p>
<p><strong>Why This Matters</strong>: - 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</p>
<hr />
<h3 id="issue-4-semantic-misuse-of-price-notation">Issue 4: Semantic
Misuse of Price Notation</h3>
<p><strong>Problem</strong>: The <code>@</code> notation in Beancount
represents <strong>acquisition cost</strong>, not <strong>settlement
value</strong>.</p>
<p><strong>Current Usage</strong>:</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @ 0.000888... EUR</code></pre>
<p><strong>What this notation means in accounting</strong>: “We
<strong>purchased</strong> 225,033 satoshis at a cost of 0.000888 EUR
per satoshi”</p>
<p><strong>What actually happened</strong>: “We
<strong>received</strong> 225,033 satoshis as payment for a debt”</p>
<p><strong>Economic Difference</strong>: - <strong>Purchase</strong>:
You exchange cash for an asset (buying Bitcoin) - <strong>Payment
Receipt</strong>: You receive an asset in settlement of a receivable</p>
<p><strong>Accounting Substance vs. Form</strong>: -
<strong>Form</strong>: The transaction looks like a Bitcoin purchase -
<strong>Substance</strong>: The transaction is actually a receivable
collection</p>
<p><strong>GAAP Principle (ASC 105-10-05)</strong>: &gt; “Accounting
should reflect the economic substance of transactions, not merely their
legal form.”</p>
<p><strong>Why This Creates Issues</strong>:</p>
<ol type="1">
<li><strong>Cost Basis Tracking</strong>: For tax purposes, the “cost”
of Bitcoin received as payment should be its fair market value at
receipt, not the receivable amount</li>
<li><strong>Price Database Pollution</strong>: Beancounts price
database now contains “prices” that arent real market prices</li>
<li><strong>Auditor Confusion</strong>: An auditor reviewing this would
question why purchase prices dont match market rates</li>
</ol>
<p><strong>Proper Accounting Approach</strong>:</p>
<pre class="beancount"><code>; 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: &quot;payment-received&quot;
Revenue:Exchange-Gain 0.50 EUR
Assets:Receivable:User-375ec158 -200.00 EUR
; Approach 2: Don&#39;t use @ notation at all
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
fmv-at-receipt: &quot;199.50 EUR&quot;
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<hr />
<h3 id="issue-5-misnamed-function-and-incorrect-usage">Issue 5: Misnamed
Function and Incorrect Usage</h3>
<p><strong>Problem</strong>: Function is called
<code>format_net_settlement_entry</code>, but its used for simple
payments that arent true net settlements.</p>
<p><strong>Example from Users Transaction</strong>: - Receivable:
200.00 EUR - Payable: 0.00 EUR - Net: 200.00 EUR (this is just a
<strong>payment</strong>, not a <strong>settlement</strong>)</p>
<p><strong>Accounting Terminology</strong>:</p>
<ul>
<li><strong>Payment</strong>: Settling a single obligation (receivable
OR payable)</li>
<li><strong>Net Settlement</strong>: Offsetting multiple obligations
(receivable AND payable)</li>
</ul>
<p><strong>When Net Settlement is Appropriate</strong>:</p>
<pre><code>User owes Castle: 555.00 EUR (receivable)
Castle owes User: 38.00 EUR (payable)
Net amount due: 517.00 EUR (true settlement)</code></pre>
<p>Proper three-posting entry:</p>
<pre class="beancount"><code>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 ✓</code></pre>
<p><strong>When Two Postings Suffice</strong>:</p>
<pre><code>User owes Castle: 200.00 EUR (receivable)
Castle owes User: 0.00 EUR (no payable)
Amount due: 200.00 EUR (simple payment)</code></pre>
<p>Simpler two-posting entry:</p>
<pre class="beancount"><code>Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
Assets:Receivable:User -200.00 EUR</code></pre>
<p><strong>Best Practice</strong>: Use the simplest journal entry
structure that accurately represents the transaction.</p>
<p><strong>Recommendation</strong>: 1. Rename function to
<code>format_payment_entry</code> or
<code>format_receivable_payment_entry</code> 2. Create separate
<code>format_net_settlement_entry</code> for true netting scenarios 3.
Use conditional logic to choose 2-posting vs 3-posting based on whether
both receivables AND payables exist</p>
<hr />
<h2 id="traditional-accounting-approaches">Traditional Accounting
Approaches</h2>
<h3
id="approach-1-record-bitcoin-at-fair-market-value-tax-compliant">Approach
1: Record Bitcoin at Fair Market Value (Tax Compliant)</h3>
<pre class="beancount"><code>2025-11-12 * &quot;Bitcoin payment from user 375ec158&quot;
Assets:Bitcoin:Lightning 199.50 EUR
sats-received: &quot;225033&quot;
fmv-per-sat: &quot;0.000886 EUR&quot;
cost-basis: &quot;199.50 EUR&quot;
payment-hash: &quot;8d080ec4...&quot;
Revenue:Exchange-Gain 0.50 EUR
source: &quot;cryptocurrency-receipt&quot;
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<p><strong>Pros</strong>: - ✅ Tax compliant (establishes cost basis) -
✅ Recognizes exchange gain/loss - ✅ Uses actual market rates - ✅
Audit trail for cryptocurrency receipts</p>
<p><strong>Cons</strong>: - ❌ Requires real-time price feeds - ❌
Creates taxable events</p>
<hr />
<h3
id="approach-2-simplified-eur-only-ledger-no-sats-positions">Approach 2:
Simplified EUR-Only Ledger (No SATS Positions)</h3>
<pre class="beancount"><code>2025-11-12 * &quot;Bitcoin payment from user 375ec158&quot;
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
sats-rate: &quot;1125.165&quot;
payment-hash: &quot;8d080ec4...&quot;
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<p><strong>Pros</strong>: - ✅ Simple and clean - ✅ EUR positions match
accounting reality - ✅ SATS tracked in metadata for reference - ✅ No
artificial price notation</p>
<p><strong>Cons</strong>: - ❌ SATS not queryable via Beancount
positions - ❌ Requires metadata parsing for SATS balances</p>
<hr />
<h3
id="approach-3-true-net-settlement-when-both-obligations-exist">Approach
3: True Net Settlement (When Both Obligations Exist)</h3>
<pre class="beancount"><code>2025-11-12 * &quot;Net settlement via Lightning&quot;
; User owes 555 EUR, Castle owes 38 EUR, net: 517 EUR
Assets:Bitcoin:Lightning 517.00 EUR
sats-received: &quot;565251&quot;
Assets:Receivable:User-375ec158 -555.00 EUR
Liabilities:Payable:User-375ec158 38.00 EUR</code></pre>
<p><strong>When to Use</strong>: Only when <strong>both</strong>
receivables and payables exist and youre truly netting them.</p>
<hr />
<h2 id="recommendations">Recommendations</h2>
<h3 id="priority-1-immediate-fixes-easy-wins">Priority 1: Immediate
Fixes (Easy Wins)</h3>
<h4 id="remove-zero-amount-postings">1.1 Remove Zero-Amount
Postings</h4>
<p><strong>File</strong>: <code>beancount_format.py:739-760</code></p>
<p><strong>Current Code</strong>:</p>
<div class="sourceCode" id="cb23"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb23-1"><a href="#cb23-1" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb23-2"><a href="#cb23-2" aria-hidden="true" tabindex="-1"></a> {...}, <span class="co"># Lightning</span></span>
<span id="cb23-3"><a href="#cb23-3" aria-hidden="true" tabindex="-1"></a> {...}, <span class="co"># Receivable</span></span>
<span id="cb23-4"><a href="#cb23-4" aria-hidden="true" tabindex="-1"></a> { <span class="co"># Payable (always included, even if 0.00)</span></span>
<span id="cb23-5"><a href="#cb23-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payable_account,</span>
<span id="cb23-6"><a href="#cb23-6" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(total_payable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb23-7"><a href="#cb23-7" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {}</span>
<span id="cb23-8"><a href="#cb23-8" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb23-9"><a href="#cb23-9" aria-hidden="true" tabindex="-1"></a>]</span></code></pre></div>
<p><strong>Fixed Code</strong>:</p>
<div class="sourceCode" id="cb24"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb24-1"><a href="#cb24-1" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb24-2"><a href="#cb24-2" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb24-3"><a href="#cb24-3" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payment_account,</span>
<span id="cb24-4"><a href="#cb24-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(amount_sats)<span class="sc">}</span><span class="ss"> SATS @@ </span><span class="sc">{</span><span class="bu">abs</span>(net_fiat_amount)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb24-5"><a href="#cb24-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;payment-hash&quot;</span>: payment_hash} <span class="cf">if</span> payment_hash <span class="cf">else</span> {}</span>
<span id="cb24-6"><a href="#cb24-6" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb24-7"><a href="#cb24-7" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb24-8"><a href="#cb24-8" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: receivable_account,</span>
<span id="cb24-9"><a href="#cb24-9" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;-</span><span class="sc">{</span><span class="bu">abs</span>(total_receivable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb24-10"><a href="#cb24-10" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;sats-equivalent&quot;</span>: <span class="bu">str</span>(<span class="bu">abs</span>(amount_sats))}</span>
<span id="cb24-11"><a href="#cb24-11" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb24-12"><a href="#cb24-12" aria-hidden="true" tabindex="-1"></a>]</span>
<span id="cb24-13"><a href="#cb24-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb24-14"><a href="#cb24-14" aria-hidden="true" tabindex="-1"></a><span class="co"># Only add payable posting if there&#39;s actually a payable to clear</span></span>
<span id="cb24-15"><a href="#cb24-15" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> total_payable_fiat <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb24-16"><a href="#cb24-16" aria-hidden="true" tabindex="-1"></a> postings.append({</span>
<span id="cb24-17"><a href="#cb24-17" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payable_account,</span>
<span id="cb24-18"><a href="#cb24-18" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(total_payable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb24-19"><a href="#cb24-19" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {}</span>
<span id="cb24-20"><a href="#cb24-20" aria-hidden="true" tabindex="-1"></a> })</span></code></pre></div>
<p><strong>Impact</strong>: Cleaner journal, professional presentation,
easier auditing</p>
<hr />
<h4 id="choose-one-sats-tracking-method">1.2 Choose One SATS Tracking
Method</h4>
<p><strong>Decision Required</strong>: Select either position-based OR
metadata-based satoshi tracking.</p>
<p><strong>Option A - Keep Metadata Approach</strong> (recommended for
Castle):</p>
<div class="sourceCode" id="cb25"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb25-1"><a href="#cb25-1" aria-hidden="true" tabindex="-1"></a><span class="co"># In format_net_settlement_entry()</span></span>
<span id="cb25-2"><a href="#cb25-2" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb25-3"><a href="#cb25-3" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb25-4"><a href="#cb25-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payment_account,</span>
<span id="cb25-5"><a href="#cb25-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(net_fiat_amount)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>, <span class="co"># EUR only</span></span>
<span id="cb25-6"><a href="#cb25-6" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {</span>
<span id="cb25-7"><a href="#cb25-7" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;sats-received&quot;</span>: <span class="bu">str</span>(<span class="bu">abs</span>(amount_sats)),</span>
<span id="cb25-8"><a href="#cb25-8" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;payment-hash&quot;</span>: payment_hash</span>
<span id="cb25-9"><a href="#cb25-9" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb25-10"><a href="#cb25-10" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb25-11"><a href="#cb25-11" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb25-12"><a href="#cb25-12" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: receivable_account,</span>
<span id="cb25-13"><a href="#cb25-13" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;-</span><span class="sc">{</span><span class="bu">abs</span>(total_receivable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb25-14"><a href="#cb25-14" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;sats-cleared&quot;</span>: <span class="bu">str</span>(<span class="bu">abs</span>(amount_sats))}</span>
<span id="cb25-15"><a href="#cb25-15" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb25-16"><a href="#cb25-16" aria-hidden="true" tabindex="-1"></a>]</span></code></pre></div>
<p><strong>Option B - Use Position-Based Tracking</strong>:</p>
<div class="sourceCode" id="cb26"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb26-1"><a href="#cb26-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Remove sats-equivalent metadata entirely</span></span>
<span id="cb26-2"><a href="#cb26-2" aria-hidden="true" tabindex="-1"></a>postings <span class="op">=</span> [</span>
<span id="cb26-3"><a href="#cb26-3" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb26-4"><a href="#cb26-4" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: payment_account,</span>
<span id="cb26-5"><a href="#cb26-5" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(amount_sats)<span class="sc">}</span><span class="ss"> SATS @@ </span><span class="sc">{</span><span class="bu">abs</span>(net_fiat_amount)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb26-6"><a href="#cb26-6" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {<span class="st">&quot;payment-hash&quot;</span>: payment_hash}</span>
<span id="cb26-7"><a href="#cb26-7" aria-hidden="true" tabindex="-1"></a> },</span>
<span id="cb26-8"><a href="#cb26-8" aria-hidden="true" tabindex="-1"></a> {</span>
<span id="cb26-9"><a href="#cb26-9" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: receivable_account,</span>
<span id="cb26-10"><a href="#cb26-10" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;-</span><span class="sc">{</span><span class="bu">abs</span>(total_receivable_fiat)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb26-11"><a href="#cb26-11" aria-hidden="true" tabindex="-1"></a> <span class="co"># No sats-equivalent needed - queryable via price database</span></span>
<span id="cb26-12"><a href="#cb26-12" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb26-13"><a href="#cb26-13" aria-hidden="true" tabindex="-1"></a>]</span></code></pre></div>
<p><strong>Recommendation</strong>: Choose Option A (metadata) for
consistency with Castles architecture.</p>
<hr />
<h4 id="rename-function-for-clarity">1.3 Rename Function for
Clarity</h4>
<p><strong>File</strong>: <code>beancount_format.py</code></p>
<p><strong>Current</strong>:
<code>format_net_settlement_entry()</code></p>
<p><strong>New</strong>: <code>format_receivable_payment_entry()</code>
or <code>format_payment_settlement_entry()</code></p>
<p><strong>Rationale</strong>: More accurately describes what the
function does (processes payments, not always net settlements)</p>
<hr />
<h3 id="priority-2-medium-term-improvements-compliance">Priority 2:
Medium-Term Improvements (Compliance)</h3>
<h4 id="add-exchange-gainloss-tracking">2.1 Add Exchange Gain/Loss
Tracking</h4>
<p><strong>File</strong>: <code>tasks.py:259-276</code> (get balance and
calculate settlement)</p>
<p><strong>New Logic</strong>:</p>
<div class="sourceCode" id="cb27"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb27-1"><a href="#cb27-1" aria-hidden="true" tabindex="-1"></a><span class="co"># Get user&#39;s current balance</span></span>
<span id="cb27-2"><a href="#cb27-2" aria-hidden="true" tabindex="-1"></a>balance <span class="op">=</span> <span class="cf">await</span> fava.get_user_balance(user_id)</span>
<span id="cb27-3"><a href="#cb27-3" aria-hidden="true" tabindex="-1"></a>fiat_balances <span class="op">=</span> balance.get(<span class="st">&quot;fiat_balances&quot;</span>, {})</span>
<span id="cb27-4"><a href="#cb27-4" aria-hidden="true" tabindex="-1"></a>total_fiat_balance <span class="op">=</span> fiat_balances.get(fiat_currency, Decimal(<span class="dv">0</span>))</span>
<span id="cb27-5"><a href="#cb27-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb27-6"><a href="#cb27-6" aria-hidden="true" tabindex="-1"></a><span class="co"># Calculate expected fiat value of SATS payment at current market rate</span></span>
<span id="cb27-7"><a href="#cb27-7" aria-hidden="true" tabindex="-1"></a>market_rate <span class="op">=</span> <span class="cf">await</span> get_current_sats_eur_rate() <span class="co"># New function needed</span></span>
<span id="cb27-8"><a href="#cb27-8" aria-hidden="true" tabindex="-1"></a>market_value <span class="op">=</span> Decimal(amount_sats) <span class="op">*</span> market_rate</span>
<span id="cb27-9"><a href="#cb27-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb27-10"><a href="#cb27-10" aria-hidden="true" tabindex="-1"></a><span class="co"># Calculate exchange variance</span></span>
<span id="cb27-11"><a href="#cb27-11" aria-hidden="true" tabindex="-1"></a>receivable_amount <span class="op">=</span> <span class="bu">abs</span>(total_fiat_balance) <span class="cf">if</span> total_fiat_balance <span class="op">&gt;</span> <span class="dv">0</span> <span class="cf">else</span> Decimal(<span class="dv">0</span>)</span>
<span id="cb27-12"><a href="#cb27-12" aria-hidden="true" tabindex="-1"></a>exchange_variance <span class="op">=</span> market_value <span class="op">-</span> receivable_amount</span>
<span id="cb27-13"><a href="#cb27-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb27-14"><a href="#cb27-14" aria-hidden="true" tabindex="-1"></a><span class="co"># If variance is material (&gt; 1 cent), create exchange gain/loss posting</span></span>
<span id="cb27-15"><a href="#cb27-15" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="bu">abs</span>(exchange_variance) <span class="op">&gt;</span> Decimal(<span class="st">&quot;0.01&quot;</span>):</span>
<span id="cb27-16"><a href="#cb27-16" aria-hidden="true" tabindex="-1"></a> <span class="co"># Add exchange gain/loss to postings</span></span>
<span id="cb27-17"><a href="#cb27-17" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> exchange_variance <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb27-18"><a href="#cb27-18" aria-hidden="true" tabindex="-1"></a> <span class="co"># Gain: payment worth more than receivable</span></span>
<span id="cb27-19"><a href="#cb27-19" aria-hidden="true" tabindex="-1"></a> exchange_account <span class="op">=</span> <span class="st">&quot;Revenue:Foreign-Exchange-Gain&quot;</span></span>
<span id="cb27-20"><a href="#cb27-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb27-21"><a href="#cb27-21" aria-hidden="true" tabindex="-1"></a> <span class="co"># Loss: payment worth less than receivable</span></span>
<span id="cb27-22"><a href="#cb27-22" aria-hidden="true" tabindex="-1"></a> exchange_account <span class="op">=</span> <span class="st">&quot;Expenses:Foreign-Exchange-Loss&quot;</span></span>
<span id="cb27-23"><a href="#cb27-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb27-24"><a href="#cb27-24" aria-hidden="true" tabindex="-1"></a> <span class="co"># Include in entry creation</span></span>
<span id="cb27-25"><a href="#cb27-25" aria-hidden="true" tabindex="-1"></a> exchange_posting <span class="op">=</span> {</span>
<span id="cb27-26"><a href="#cb27-26" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;account&quot;</span>: exchange_account,</span>
<span id="cb27-27"><a href="#cb27-27" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;amount&quot;</span>: <span class="ss">f&quot;</span><span class="sc">{</span><span class="bu">abs</span>(exchange_variance)<span class="sc">:.2f}</span><span class="ss"> </span><span class="sc">{</span>fiat_currency<span class="sc">}</span><span class="ss">&quot;</span>,</span>
<span id="cb27-28"><a href="#cb27-28" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;meta&quot;</span>: {</span>
<span id="cb27-29"><a href="#cb27-29" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;sats-amount&quot;</span>: <span class="bu">str</span>(amount_sats),</span>
<span id="cb27-30"><a href="#cb27-30" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;market-rate&quot;</span>: <span class="bu">str</span>(market_rate),</span>
<span id="cb27-31"><a href="#cb27-31" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;receivable-amount&quot;</span>: <span class="bu">str</span>(receivable_amount)</span>
<span id="cb27-32"><a href="#cb27-32" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb27-33"><a href="#cb27-33" aria-hidden="true" tabindex="-1"></a> }</span></code></pre></div>
<p><strong>Benefits</strong>: - ✅ Tax compliance - ✅ Accurate
financial reporting - ✅ Audit trail for cryptocurrency gains/losses -
✅ Regulatory compliance (GAAP/IFRS)</p>
<hr />
<h4 id="implement-true-net-settlement-vs.-simple-payment-logic">2.2
Implement True Net Settlement vs. Simple Payment Logic</h4>
<p><strong>File</strong>: <code>tasks.py</code> or new
<code>payment_logic.py</code></p>
<div class="sourceCode" id="cb28"><pre
class="sourceCode python"><code class="sourceCode python"><span id="cb28-1"><a href="#cb28-1" aria-hidden="true" tabindex="-1"></a><span class="cf">async</span> <span class="kw">def</span> create_payment_entry(</span>
<span id="cb28-2"><a href="#cb28-2" aria-hidden="true" tabindex="-1"></a> user_id: <span class="bu">str</span>,</span>
<span id="cb28-3"><a href="#cb28-3" aria-hidden="true" tabindex="-1"></a> amount_sats: <span class="bu">int</span>,</span>
<span id="cb28-4"><a href="#cb28-4" aria-hidden="true" tabindex="-1"></a> fiat_amount: Decimal,</span>
<span id="cb28-5"><a href="#cb28-5" aria-hidden="true" tabindex="-1"></a> fiat_currency: <span class="bu">str</span>,</span>
<span id="cb28-6"><a href="#cb28-6" aria-hidden="true" tabindex="-1"></a> payment_hash: <span class="bu">str</span></span>
<span id="cb28-7"><a href="#cb28-7" aria-hidden="true" tabindex="-1"></a>):</span>
<span id="cb28-8"><a href="#cb28-8" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb28-9"><a href="#cb28-9" aria-hidden="true" tabindex="-1"></a><span class="co"> Create appropriate payment entry based on user&#39;s balance situation.</span></span>
<span id="cb28-10"><a href="#cb28-10" aria-hidden="true" tabindex="-1"></a><span class="co"> Uses 2-posting for simple payments, 3-posting for net settlements.</span></span>
<span id="cb28-11"><a href="#cb28-11" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb28-12"><a href="#cb28-12" aria-hidden="true" tabindex="-1"></a> <span class="co"># Get user balance</span></span>
<span id="cb28-13"><a href="#cb28-13" aria-hidden="true" tabindex="-1"></a> balance <span class="op">=</span> <span class="cf">await</span> fava.get_user_balance(user_id)</span>
<span id="cb28-14"><a href="#cb28-14" aria-hidden="true" tabindex="-1"></a> fiat_balances <span class="op">=</span> balance.get(<span class="st">&quot;fiat_balances&quot;</span>, {})</span>
<span id="cb28-15"><a href="#cb28-15" aria-hidden="true" tabindex="-1"></a> total_balance <span class="op">=</span> fiat_balances.get(fiat_currency, Decimal(<span class="dv">0</span>))</span>
<span id="cb28-16"><a href="#cb28-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb28-17"><a href="#cb28-17" aria-hidden="true" tabindex="-1"></a> receivable_amount <span class="op">=</span> Decimal(<span class="dv">0</span>)</span>
<span id="cb28-18"><a href="#cb28-18" aria-hidden="true" tabindex="-1"></a> payable_amount <span class="op">=</span> Decimal(<span class="dv">0</span>)</span>
<span id="cb28-19"><a href="#cb28-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb28-20"><a href="#cb28-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> total_balance <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb28-21"><a href="#cb28-21" aria-hidden="true" tabindex="-1"></a> receivable_amount <span class="op">=</span> total_balance</span>
<span id="cb28-22"><a href="#cb28-22" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> total_balance <span class="op">&lt;</span> <span class="dv">0</span>:</span>
<span id="cb28-23"><a href="#cb28-23" aria-hidden="true" tabindex="-1"></a> payable_amount <span class="op">=</span> <span class="bu">abs</span>(total_balance)</span>
<span id="cb28-24"><a href="#cb28-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb28-25"><a href="#cb28-25" aria-hidden="true" tabindex="-1"></a> <span class="co"># Determine entry type</span></span>
<span id="cb28-26"><a href="#cb28-26" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> receivable_amount <span class="op">&gt;</span> <span class="dv">0</span> <span class="kw">and</span> payable_amount <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb28-27"><a href="#cb28-27" aria-hidden="true" tabindex="-1"></a> <span class="co"># TRUE NET SETTLEMENT: Both obligations exist</span></span>
<span id="cb28-28"><a href="#cb28-28" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="cf">await</span> format_net_settlement_entry(</span>
<span id="cb28-29"><a href="#cb28-29" aria-hidden="true" tabindex="-1"></a> user_id<span class="op">=</span>user_id,</span>
<span id="cb28-30"><a href="#cb28-30" aria-hidden="true" tabindex="-1"></a> amount_sats<span class="op">=</span>amount_sats,</span>
<span id="cb28-31"><a href="#cb28-31" aria-hidden="true" tabindex="-1"></a> receivable_amount<span class="op">=</span>receivable_amount,</span>
<span id="cb28-32"><a href="#cb28-32" aria-hidden="true" tabindex="-1"></a> payable_amount<span class="op">=</span>payable_amount,</span>
<span id="cb28-33"><a href="#cb28-33" aria-hidden="true" tabindex="-1"></a> fiat_amount<span class="op">=</span>fiat_amount,</span>
<span id="cb28-34"><a href="#cb28-34" aria-hidden="true" tabindex="-1"></a> fiat_currency<span class="op">=</span>fiat_currency,</span>
<span id="cb28-35"><a href="#cb28-35" aria-hidden="true" tabindex="-1"></a> payment_hash<span class="op">=</span>payment_hash</span>
<span id="cb28-36"><a href="#cb28-36" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb28-37"><a href="#cb28-37" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> receivable_amount <span class="op">&gt;</span> <span class="dv">0</span>:</span>
<span id="cb28-38"><a href="#cb28-38" aria-hidden="true" tabindex="-1"></a> <span class="co"># SIMPLE RECEIVABLE PAYMENT: Only receivable exists</span></span>
<span id="cb28-39"><a href="#cb28-39" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="cf">await</span> format_receivable_payment_entry(</span>
<span id="cb28-40"><a href="#cb28-40" aria-hidden="true" tabindex="-1"></a> user_id<span class="op">=</span>user_id,</span>
<span id="cb28-41"><a href="#cb28-41" aria-hidden="true" tabindex="-1"></a> amount_sats<span class="op">=</span>amount_sats,</span>
<span id="cb28-42"><a href="#cb28-42" aria-hidden="true" tabindex="-1"></a> receivable_amount<span class="op">=</span>receivable_amount,</span>
<span id="cb28-43"><a href="#cb28-43" aria-hidden="true" tabindex="-1"></a> fiat_amount<span class="op">=</span>fiat_amount,</span>
<span id="cb28-44"><a href="#cb28-44" aria-hidden="true" tabindex="-1"></a> fiat_currency<span class="op">=</span>fiat_currency,</span>
<span id="cb28-45"><a href="#cb28-45" aria-hidden="true" tabindex="-1"></a> payment_hash<span class="op">=</span>payment_hash</span>
<span id="cb28-46"><a href="#cb28-46" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb28-47"><a href="#cb28-47" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb28-48"><a href="#cb28-48" aria-hidden="true" tabindex="-1"></a> <span class="co"># PAYABLE PAYMENT: Castle paying user (different flow)</span></span>
<span id="cb28-49"><a href="#cb28-49" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="cf">await</span> format_payable_payment_entry(...)</span></code></pre></div>
<hr />
<h3 id="priority-3-long-term-architectural-decisions">Priority 3:
Long-Term Architectural Decisions</h3>
<h4 id="establish-primary-currency-hierarchy">3.1 Establish Primary
Currency Hierarchy</h4>
<p><strong>Current Issue</strong>: Mixed approach (EUR positions with
SATS metadata, but also SATS positions with @ notation)</p>
<p><strong>Decision Required</strong>: Choose ONE of the following
architectures:</p>
<p><strong>Architecture A - EUR Primary, SATS Secondary</strong>
(recommended):</p>
<pre class="beancount"><code>; All positions in EUR, SATS in metadata
2025-11-12 * &quot;Payment&quot;
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
Assets:Receivable:User -200.00 EUR
sats-cleared: &quot;225033&quot;</code></pre>
<p><strong>Architecture B - SATS Primary, EUR Secondary</strong>:</p>
<pre class="beancount"><code>; All positions in SATS, EUR in metadata
2025-11-12 * &quot;Payment&quot;
Assets:Bitcoin:Lightning 225033 SATS
eur-value: &quot;200.00&quot;
Assets:Receivable:User -225033 SATS
eur-cleared: &quot;200.00&quot;</code></pre>
<p><strong>Recommendation</strong>: 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</p>
<hr />
<h4 id="consider-separate-ledger-for-cryptocurrency-holdings">3.2
Consider Separate Ledger for Cryptocurrency Holdings</h4>
<p><strong>Advanced Approach</strong>: Separate cryptocurrency movements
from fiat accounting</p>
<p><strong>Main Ledger</strong> (EUR-denominated):</p>
<pre class="beancount"><code>2025-11-12 * &quot;Payment received from user&quot;
Assets:Bitcoin-Custody:User-375ec158 200.00 EUR
Assets:Receivable:User-375ec158 -200.00 EUR</code></pre>
<p><strong>Cryptocurrency Sub-Ledger</strong> (SATS-denominated):</p>
<pre class="beancount"><code>2025-11-12 * &quot;Lightning payment received&quot;
Assets:Bitcoin:Lightning:Castle 225033 SATS
Assets:Bitcoin:Custody:User-375ec 225033 SATS</code></pre>
<p><strong>Benefits</strong>: - ✅ Clean separation of concerns - ✅
Cryptocurrency movements tracked independently - ✅ Fiat accounting
unaffected by Bitcoin volatility - ✅ Can generate separate financial
statements</p>
<p><strong>Drawbacks</strong>: - ❌ Increased complexity - ❌
Reconciliation between ledgers required - ❌ Two sets of books to
maintain</p>
<hr />
<h2 id="code-files-requiring-changes">Code Files Requiring Changes</h2>
<h3 id="high-priority-immediate-fixes">High Priority (Immediate
Fixes)</h3>
<ol type="1">
<li><strong><code>beancount_format.py:739-760</code></strong>
<ul>
<li>Remove zero-amount postings</li>
<li>Make payable posting conditional</li>
</ul></li>
<li><strong><code>beancount_format.py:692</code></strong>
<ul>
<li>Rename function to <code>format_receivable_payment_entry</code></li>
</ul></li>
</ol>
<h3 id="medium-priority-compliance">Medium Priority (Compliance)</h3>
<ol start="3" type="1">
<li><strong><code>tasks.py:235-310</code></strong>
<ul>
<li>Add exchange gain/loss calculation</li>
<li>Implement payment vs. settlement logic</li>
</ul></li>
<li><strong>New file: <code>exchange_rates.py</code></strong>
<ul>
<li>Create <code>get_current_sats_eur_rate()</code> function</li>
<li>Implement price feed integration</li>
</ul></li>
<li><strong><code>beancount_format.py</code></strong>
<ul>
<li>Create new <code>format_net_settlement_entry()</code> for true
netting</li>
<li>Create <code>format_receivable_payment_entry()</code> for simple
payments</li>
</ul></li>
</ol>
<hr />
<h2 id="testing-requirements">Testing Requirements</h2>
<h3 id="test-case-1-simple-receivable-payment-no-payable">Test Case 1:
Simple Receivable Payment (No Payable)</h3>
<p><strong>Setup</strong>: - User has receivable: 200.00 EUR - User has
payable: 0.00 EUR - User pays: 225,033 SATS</p>
<p><strong>Expected Entry</strong> (after fixes):</p>
<pre class="beancount"><code>2025-11-12 * &quot;Lightning payment from user&quot;
Assets:Bitcoin:Lightning 200.00 EUR
sats-received: &quot;225033&quot;
payment-hash: &quot;8d080ec4...&quot;
Assets:Receivable:User -200.00 EUR
sats-cleared: &quot;225033&quot;</code></pre>
<p><strong>Verify</strong>: - ✅ Only 2 postings (no zero-amount
payable) - ✅ Entry balances - ✅ SATS tracked in metadata - ✅ User
balance becomes 0 (both EUR and SATS)</p>
<hr />
<h3 id="test-case-2-true-net-settlement">Test Case 2: True Net
Settlement</h3>
<p><strong>Setup</strong>: - 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)</p>
<p><strong>Expected Entry</strong>:</p>
<pre class="beancount"><code>2025-11-12 * &quot;Net settlement via Lightning&quot;
Assets:Bitcoin:Lightning 517.00 EUR
sats-received: &quot;565251&quot;
payment-hash: &quot;abc123...&quot;
Assets:Receivable:User -555.00 EUR
sats-portion: &quot;565251&quot;
Liabilities:Payable:User 38.00 EUR</code></pre>
<p><strong>Verify</strong>: - ✅ 3 postings (receivable + payable
cleared) - ✅ Net amount = receivable - payable - ✅ Both balances
become 0 - ✅ Mathematically balanced</p>
<hr />
<h3 id="test-case-3-exchange-gainloss-future">Test Case 3: Exchange
Gain/Loss (Future)</h3>
<p><strong>Setup</strong>: - 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</p>
<p><strong>Expected Entry</strong> (with exchange tracking):</p>
<pre class="beancount"><code>2025-11-12 * &quot;Lightning payment with exchange loss&quot;
Assets:Bitcoin:Lightning 199.50 EUR
sats-received: &quot;225033&quot;
market-rate: &quot;0.000886&quot;
Expenses:Foreign-Exchange-Loss 0.50 EUR
Assets:Receivable:User -200.00 EUR</code></pre>
<p><strong>Verify</strong>: - ✅ Bitcoin recorded at fair market value -
✅ Exchange loss recognized - ✅ Receivable cleared at book value - ✅
Entry balances</p>
<hr />
<h2 id="conclusion">Conclusion</h2>
<h3 id="summary-of-issues">Summary of Issues</h3>
<table>
<colgroup>
<col style="width: 12%" />
<col style="width: 18%" />
<col style="width: 34%" />
<col style="width: 34%" />
</colgroup>
<thead>
<tr>
<th>Issue</th>
<th>Severity</th>
<th>Accounting Impact</th>
<th>Recommended Action</th>
</tr>
</thead>
<tbody>
<tr>
<td>Zero-amount postings</td>
<td>Low</td>
<td>Presentation only</td>
<td>Remove immediately</td>
</tr>
<tr>
<td>Redundant SATS tracking</td>
<td>Low</td>
<td>Storage/efficiency</td>
<td>Choose one method</td>
</tr>
<tr>
<td>No exchange gain/loss</td>
<td><strong>High</strong></td>
<td>Financial accuracy</td>
<td>Implement for compliance</td>
</tr>
<tr>
<td>Semantic misuse of @</td>
<td>Medium</td>
<td>Audit clarity</td>
<td>Consider EUR-only positions</td>
</tr>
<tr>
<td>Misnamed function</td>
<td>Low</td>
<td>Code clarity</td>
<td>Rename function</td>
</tr>
</tbody>
</table>
<h3 id="professional-assessment">Professional Assessment</h3>
<p><strong>Is this “best practice” accounting?</strong>
<strong>No</strong>, this implementation deviates from traditional
accounting standards in several ways.</p>
<p><strong>Is it acceptable for Castles use case?</strong> <strong>Yes,
with modifications</strong>, its a reasonable pragmatic solution for a
novel problem (cryptocurrency payments of fiat debts).</p>
<p><strong>Critical improvements needed</strong>: 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)</p>
<p><strong>The fundamental challenge</strong>: Traditional accounting
wasnt designed for this scenario. There is no established “standard”
for recording cryptocurrency payments of fiat-denominated receivables.
Castles approach is functional, but should be refined to align better
with accounting principles where possible.</p>
<h3 id="next-steps">Next Steps</h3>
<ol type="1">
<li><strong>Week 1</strong>: Implement Priority 1 fixes (remove zero
postings, rename function)</li>
<li><strong>Week 2-3</strong>: Design and implement exchange gain/loss
tracking</li>
<li><strong>Week 4</strong>: Add payment vs. settlement logic</li>
<li><strong>Ongoing</strong>: Monitor regulatory guidance on
cryptocurrency accounting</li>
</ol>
<hr />
<h2 id="references">References</h2>
<ul>
<li><strong>FASB ASC 830</strong>: Foreign Currency Matters</li>
<li><strong>IAS 21</strong>: The Effects of Changes in Foreign Exchange
Rates</li>
<li><strong>FASB Concept Statement No. 2</strong>: Qualitative
Characteristics of Accounting Information</li>
<li><strong>ASC 105-10-05</strong>: Substance Over Form</li>
<li><strong>Beancount Documentation</strong>:
http://furius.ca/beancount/doc/index</li>
<li><strong>Castle Extension</strong>:
<code>docs/SATS-EQUIVALENT-METADATA.md</code></li>
<li><strong>BQL Analysis</strong>:
<code>docs/BQL-BALANCE-QUERIES.md</code></li>
</ul>
<hr />
<p><strong>Document Version</strong>: 1.0 <strong>Last Updated</strong>:
2025-01-12 <strong>Next Review</strong>: After Priority 1 fixes
implemented</p>
<hr />
<p><em>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
Castles payment recording system.</em></p>
</body>
</html>

View file

@ -0,0 +1,861 @@
# Accounting Analysis: Net Settlement Entry Pattern
**Date**: 2025-01-12
**Prepared By**: Senior Accounting Review
**Subject**: Castle Extension - Lightning Payment Settlement Entries
**Status**: Technical Review
---
## Executive Summary
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
---
## Background: The Technical Challenge
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
---
## Current Implementation
### Transaction Example
```beancount
; 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
```
### Code Implementation
**Location**: `beancount_format.py:739-760`
```python
# 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)
---
## Accounting Issues Identified
### Issue 1: Zero-Amount Postings
**Problem**: The third posting often records `0.00 EUR` when no payable exists.
```beancount
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**:
```python
# 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": {}
})
```
---
### Issue 2: Redundant Satoshi Tracking
**Problem**: Satoshis are tracked in TWO places in the same transaction:
1. **Position Amount** (via `@@` notation):
```beancount
Assets:Bitcoin:Lightning 225033 SATS @@ 200.00 EUR
```
2. **Metadata** (sats-equivalent):
```beancount
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:
```beancount
; 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:
```sql
SELECT account, sum(convert(position, SATS))
WHERE account = 'Assets:Bitcoin:Lightning'
```
**Recommendation**:
Choose ONE approach consistently:
**Option A - Use @ notation** (Beancount standard):
```beancount
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):
```beancount
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)
---
### Issue 3: No Exchange Gain/Loss Recognition
**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:
```beancount
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**:
```beancount
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
---
### Issue 4: Semantic Misuse of Price Notation
**Problem**: The `@` notation in Beancount represents **acquisition cost**, not **settlement value**.
**Current Usage**:
```beancount
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**:
1. **Cost Basis Tracking**: For tax purposes, the "cost" of Bitcoin received as payment should be its fair market value at receipt, not the receivable amount
2. **Price Database Pollution**: Beancount's price database now contains "prices" that aren't real market prices
3. **Auditor Confusion**: An auditor reviewing this would question why purchase prices don't match market rates
**Proper Accounting Approach**:
```beancount
; 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
```
---
### Issue 5: Misnamed Function and Incorrect Usage
**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**:
- **Payment**: Settling a single obligation (receivable OR payable)
- **Net Settlement**: Offsetting multiple obligations (receivable AND payable)
**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:
```beancount
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:
```beancount
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
---
## Traditional Accounting Approaches
### Approach 1: Record Bitcoin at Fair Market Value (Tax Compliant)
```beancount
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
---
### Approach 2: Simplified EUR-Only Ledger (No SATS Positions)
```beancount
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
---
### Approach 3: True Net Settlement (When Both Obligations Exist)
```beancount
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.
---
## Recommendations
### Priority 1: Immediate Fixes (Easy Wins)
#### 1.1 Remove Zero-Amount Postings
**File**: `beancount_format.py:739-760`
**Current Code**:
```python
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**:
```python
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
---
#### 1.2 Choose One SATS Tracking Method
**Decision Required**: Select either position-based OR metadata-based satoshi tracking.
**Option A - Keep Metadata Approach** (recommended for Castle):
```python
# 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**:
```python
# 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.
---
#### 1.3 Rename Function for Clarity
**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)
---
### Priority 2: Medium-Term Improvements (Compliance)
#### 2.1 Add Exchange Gain/Loss Tracking
**File**: `tasks.py:259-276` (get balance and calculate settlement)
**New Logic**:
```python
# 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)
---
#### 2.2 Implement True Net Settlement vs. Simple Payment Logic
**File**: `tasks.py` or new `payment_logic.py`
```python
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(...)
```
---
### Priority 3: Long-Term Architectural Decisions
#### 3.1 Establish Primary Currency Hierarchy
**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):
```beancount
; 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**:
```beancount
; 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
---
#### 3.2 Consider Separate Ledger for Cryptocurrency Holdings
**Advanced Approach**: Separate cryptocurrency movements from fiat accounting
**Main Ledger** (EUR-denominated):
```beancount
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):
```beancount
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
---
## Code Files Requiring Changes
### High Priority (Immediate Fixes)
1. **`beancount_format.py:739-760`**
- Remove zero-amount postings
- Make payable posting conditional
2. **`beancount_format.py:692`**
- Rename function to `format_receivable_payment_entry`
### Medium Priority (Compliance)
3. **`tasks.py:235-310`**
- Add exchange gain/loss calculation
- Implement payment vs. settlement logic
4. **New file: `exchange_rates.py`**
- Create `get_current_sats_eur_rate()` function
- Implement price feed integration
5. **`beancount_format.py`**
- Create new `format_net_settlement_entry()` for true netting
- Create `format_receivable_payment_entry()` for simple payments
---
## Testing Requirements
### Test Case 1: Simple Receivable Payment (No Payable)
**Setup**:
- User has receivable: 200.00 EUR
- User has payable: 0.00 EUR
- User pays: 225,033 SATS
**Expected Entry** (after fixes):
```beancount
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)
---
### Test Case 2: True Net Settlement
**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**:
```beancount
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
---
### Test Case 3: Exchange Gain/Loss (Future)
**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):
```beancount
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
---
## Conclusion
### Summary of Issues
| 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 |
### Professional Assessment
**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.
### Next Steps
1. **Week 1**: Implement Priority 1 fixes (remove zero postings, rename function)
2. **Week 2-3**: Design and implement exchange gain/loss tracking
3. **Week 4**: Add payment vs. settlement logic
4. **Ongoing**: Monitor regulatory guidance on cryptocurrency accounting
---
## References
- **FASB ASC 830**: Foreign Currency Matters
- **IAS 21**: The Effects of Changes in Foreign Exchange Rates
- **FASB Concept Statement No. 2**: Qualitative Characteristics of Accounting Information
- **ASC 105-10-05**: Substance Over Form
- **Beancount Documentation**: http://furius.ca/beancount/doc/index
- **Castle Extension**: `docs/SATS-EQUIVALENT-METADATA.md`
- **BQL Analysis**: `docs/BQL-BALANCE-QUERIES.md`
---
**Document 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.*

View file

@ -0,0 +1,529 @@
# BQL Price Notation Solution for SATS Tracking
**Date**: 2025-01-12
**Status**: Testing
**Context**: Explore price notation as alternative to metadata for SATS tracking
---
## Problem Recap
Current approach stores SATS in metadata:
```beancount
2025-11-10 * "Groceries"
Expenses:Food -360.00 EUR
sats-equivalent: 337096
Liabilities:Payable:User-abc 360.00 EUR
sats-equivalent: 337096
```
**Issue**: BQL cannot access metadata, so balance queries require manual aggregation.
---
## Solution: Use Price Notation
### Proposed Format
Post in actual transaction currency (EUR) with SATS as price:
```beancount
2025-11-10 * "Groceries"
Expenses:Food -360.00 EUR @@ 337096 SATS
Liabilities:Payable:User-abc 360.00 EUR @@ 337096 SATS
```
**What this means**:
- Primary amount: `-360.00 EUR` (the actual transaction currency)
- Total price: `337096 SATS` (the bitcoin equivalent value)
- Transaction integrity preserved (posted in EUR as it occurred)
- SATS tracked as price (queryable by BQL)
---
## Price Notation Options
### Option 1: Per-Unit Price (`@`)
```beancount
Expenses:Food -360.00 EUR @ 936.38 SATS
```
**What it means**: Each EUR is worth 936.38 SATS
**Total calculation**: 360 × 936.38 = 337,096.8 SATS
**Precision**: May introduce rounding (336,696.8 vs 337,096)
### Option 2: Total Price (`@@`) ✅ RECOMMENDED
```beancount
Expenses:Food -360.00 EUR @@ 337096 SATS
```
**What it means**: Total transaction value is 337,096 SATS
**Total calculation**: Exact 337,096 SATS (no rounding)
**Precision**: Preserves exact SATS amount from original calculation
**Why `@@` is better for Castle:**
- ✅ Preserves exact SATS amount (no rounding errors)
- ✅ Matches current metadata storage exactly
- ✅ Clearer intent: "this transaction equals X SATS total"
---
## How BQL Handles Prices
### Available Price Columns
From BQL schema:
- `price_number` - The numeric price amount (Decimal)
- `price_currency` - The currency of the price (str)
- `position` - Full posting (includes price)
- `WEIGHT(position)` - Function that returns balance weight
### BQL Query Capabilities
**Test Query 1: Access price directly**
```sql
SELECT account, number, currency, price_number, price_currency
WHERE account ~ 'User-375ec158'
AND price_currency = 'SATS';
```
**Expected Result** (if price notation works):
```json
{
"rows": [
["Liabilities:Payable:User-abc", "360.00", "EUR", "337096", "SATS"]
]
}
```
**Test Query 2: Aggregate SATS from prices**
```sql
SELECT account,
SUM(price_number) as total_sats
WHERE account ~ 'User-'
AND price_currency = 'SATS'
AND flag != '!'
GROUP BY account;
```
**Expected Result**:
```json
{
"rows": [
["Liabilities:Payable:User-abc", "337096"]
]
}
```
---
## Testing Plan
### Step 1: Run Metadata Test
```bash
cd /home/padreug/projects/castle-beancounter
./test_metadata_simple.sh
```
**What to look for**:
- Does `meta` column exist in response?
- Is `sats-equivalent` accessible in the data?
**If YES**: Metadata IS accessible, simpler solution available
**If NO**: Proceed with price notation approach
### Step 2: Test Current Data Structure
```bash
./test_bql_metadata.sh
```
This runs 6 tests:
1. Check metadata column
2. Check price columns
3. Basic position query
4. Test WEIGHT function
5. Aggregate positions
6. Aggregate weights
**What to look for**:
- Which columns are available?
- What does `position` return for entries with prices?
- Can we access `price_number` and `price_currency`?
### Step 3: Create Test Ledger Entry
Add one test entry to your ledger:
```beancount
2025-01-12 * "TEST: Price notation test"
Expenses:Test:PriceNotation -100.00 EUR @@ 93600 SATS
Liabilities:Payable:User-TEST 100.00 EUR @@ 93600 SATS
```
Then query:
```bash
curl -s "http://localhost:3333/castle-ledger/api/query" \
-G \
--data-urlencode "query_string=SELECT account, position, price_number, price_currency WHERE account ~ 'TEST'" \
| jq '.'
```
**Expected if working**:
```json
{
"data": {
"rows": [
["Expenses:Test:PriceNotation", "-100.00 EUR @@ 93600 SATS", "93600", "SATS"],
["Liabilities:Payable:User-TEST", "100.00 EUR @@ 93600 SATS", "93600", "SATS"]
],
"types": [
{"name": "account", "type": "str"},
{"name": "position", "type": "Position"},
{"name": "price_number", "type": "Decimal"},
{"name": "price_currency", "type": "str"}
]
}
}
```
---
## Migration Strategy (If Price Notation Works)
### Phase 1: Test on Sample Data
1. Create test ledger with mix of formats
2. Verify BQL can query price_number
3. Verify aggregation accuracy
4. Compare with manual method results
### Phase 2: Write Migration Script
```python
#!/usr/bin/env python3
"""
Migrate metadata sats-equivalent to price notation.
Converts:
Expenses:Food -360.00 EUR
sats-equivalent: 337096
To:
Expenses:Food -360.00 EUR @@ 337096 SATS
"""
import re
from pathlib import Path
def migrate_entry(entry_lines):
"""Migrate a single transaction entry."""
result = []
current_posting = None
sats_value = None
for line in entry_lines:
# Check if this is a posting line
if re.match(r'^\s{2,}\w+:', line):
# If we have pending sats from previous posting, add it
if current_posting and sats_value:
# Add @@ notation to posting
posting = current_posting.rstrip()
posting += f" @@ {sats_value} SATS\n"
result.append(posting)
current_posting = None
sats_value = None
else:
if current_posting:
result.append(current_posting)
current_posting = line
# Check if this is sats-equivalent metadata
elif 'sats-equivalent:' in line:
match = re.search(r'sats-equivalent:\s*(-?\d+)', line)
if match:
sats_value = match.group(1)
# Don't include metadata line in result
else:
# Other lines (date, narration, other metadata)
if current_posting and sats_value:
posting = current_posting.rstrip()
posting += f" @@ {sats_value} SATS\n"
result.append(posting)
current_posting = None
sats_value = None
elif current_posting:
result.append(current_posting)
current_posting = None
result.append(line)
# Handle last posting
if current_posting and sats_value:
posting = current_posting.rstrip()
posting += f" @@ {sats_value} SATS\n"
result.append(posting)
elif current_posting:
result.append(current_posting)
return result
def migrate_ledger(input_file, output_file):
"""Migrate entire ledger file."""
with open(input_file, 'r') as f:
lines = f.readlines()
result = []
current_entry = []
in_transaction = False
for line in lines:
# Transaction start
if re.match(r'^\d{4}-\d{2}-\d{2}\s+[*!]', line):
in_transaction = True
current_entry = [line]
# Empty line ends transaction
elif in_transaction and line.strip() == '':
current_entry.append(line)
migrated = migrate_entry(current_entry)
result.extend(migrated)
current_entry = []
in_transaction = False
# Inside transaction
elif in_transaction:
current_entry.append(line)
# Outside transaction
else:
result.append(line)
# Handle last entry if file doesn't end with blank line
if current_entry:
migrated = migrate_entry(current_entry)
result.extend(migrated)
with open(output_file, 'w') as f:
f.writelines(result)
if __name__ == '__main__':
import sys
if len(sys.argv) != 3:
print("Usage: migrate_ledger.py <input.beancount> <output.beancount>")
sys.exit(1)
migrate_ledger(sys.argv[1], sys.argv[2])
print(f"Migrated {sys.argv[1]} -> {sys.argv[2]}")
```
### Phase 3: Update Balance Query Methods
Replace `get_user_balance_bql()` with price-based version:
```python
async def get_user_balance_bql(self, user_id: str) -> Dict[str, Any]:
"""
Get user balance using price notation (SATS stored as @@ price).
Returns:
{
"balance": int (sats from price_number),
"fiat_balances": {"EUR": Decimal("100.50")},
"accounts": [{"account": "...", "sats": 150000}]
}
"""
user_id_prefix = user_id[:8]
# Query: Get EUR positions with SATS prices
query = f"""
SELECT
account,
number as eur_amount,
price_number as sats_amount
WHERE account ~ ':User-{user_id_prefix}'
AND (account ~ 'Payable' OR account ~ 'Receivable')
AND flag != '!'
AND price_currency = 'SATS'
"""
result = await self.query_bql(query)
total_sats = 0
fiat_balances = {}
accounts_map = {}
for row in result["rows"]:
account_name, eur_amount, sats_amount = row
# Parse amounts
sats = int(Decimal(sats_amount)) if sats_amount else 0
eur = Decimal(eur_amount) if eur_amount else Decimal(0)
total_sats += sats
# Aggregate fiat
if eur != 0:
if "EUR" not in fiat_balances:
fiat_balances["EUR"] = Decimal(0)
fiat_balances["EUR"] += eur
# Track per account
if account_name not in accounts_map:
accounts_map[account_name] = {"account": account_name, "sats": 0}
accounts_map[account_name]["sats"] += sats
return {
"balance": total_sats,
"fiat_balances": fiat_balances,
"accounts": list(accounts_map.values())
}
```
### Phase 4: Validation
1. Run both methods in parallel
2. Compare results for all users
3. Log any discrepancies
4. Investigate and fix differences
5. Once validated, switch to BQL method
---
## Advantages of Price Notation Approach
### 1. BQL Compatibility ✅
- `price_number` is a standard BQL column
- Can aggregate: `SUM(price_number)`
- Can filter: `WHERE price_currency = 'SATS'`
### 2. Transaction Integrity ✅
- Post in actual transaction currency (EUR)
- SATS as secondary value (price)
- Proper accounting: source currency preserved
### 3. Beancount Features ✅
- Price database automatically updated
- Can query historical EUR/SATS rates
- Reports can show both EUR and SATS values
### 4. Performance ✅
- BQL filters at source (no fetching all entries)
- Direct column access (no metadata parsing)
- Efficient aggregation (database-level)
### 5. Reporting Flexibility ✅
- Show EUR amounts in reports
- Show SATS equivalents alongside
- Filter by either currency
- Calculate gains/losses if SATS price changes
---
## Potential Issues and Solutions
### Issue 1: Price vs Cost Confusion
**Problem**: Beancount distinguishes between `@` price and `{}` cost
**Solution**: Always use price (`@` or `@@`), never cost (`{}`)
**Why**:
- Cost is for tracking cost basis (investments, capital gains)
- Price is for conversion rates (what we need)
### Issue 2: Precision Loss with `@`
**Problem**: Per-unit price may have rounding
```beancount
360.00 EUR @ 936.38 SATS = 336,696.8 SATS (not 337,096)
```
**Solution**: Always use `@@` total price
```beancount
360.00 EUR @@ 337096 SATS = 337,096 SATS (exact)
```
### Issue 3: Negative Numbers
**Problem**: How to handle negative EUR with positive SATS?
```beancount
-360.00 EUR @@ ??? SATS
```
**Solution**: Price is always positive (it's a rate, not an amount)
```beancount
-360.00 EUR @@ 337096 SATS ✅ Correct
```
The sign applies to the position, price is the conversion factor.
### Issue 4: Historical Data
**Problem**: Existing entries have metadata, not prices
**Solution**: Migration script (see Phase 2)
- One-time conversion
- Validate with checksums
- Keep backup of original
---
## Testing Checklist
- [ ] Run `test_metadata_simple.sh` - Check if metadata is accessible
- [ ] Run `test_bql_metadata.sh` - Full BQL capabilities test
- [ ] Add test entry with `@@` notation to ledger
- [ ] Query test entry with BQL to verify price_number access
- [ ] Compare aggregation: metadata vs price notation
- [ ] Test negative amounts with prices
- [ ] Test zero amounts
- [ ] Test multi-currency scenarios (EUR, USD with SATS prices)
- [ ] Verify price database is populated correctly
- [ ] Check that WEIGHT() function returns SATS value
- [ ] Validate balances match current manual method
---
## Decision Matrix
| Criteria | Metadata | Price Notation | Winner |
|----------|----------|----------------|--------|
| BQL Queryable | ❌ No | ✅ Yes | Price |
| Transaction Integrity | ✅ EUR first | ✅ EUR first | Tie |
| SATS Precision | ✅ Exact int | ✅ Exact (with @@) | Tie |
| Migration Effort | ✅ None | ⚠️ Script needed | Metadata |
| Performance | ❌ Manual loop | ✅ BQL optimized | Price |
| Beancount Standard | ⚠️ Non-standard | ✅ Standard feature | Price |
| Reporting Flexibility | ⚠️ Limited | ✅ Both currencies | Price |
| Future Proof | ⚠️ Custom | ✅ Standard | Price |
**Recommendation**: **Price Notation** if tests confirm BQL can access `price_number`
---
## Next Steps
1. **Run tests** (test_metadata_simple.sh and test_bql_metadata.sh)
2. **Review results** - Can BQL access price_number?
3. **Add test entry** with @@ notation
4. **Query test entry** - Verify aggregation works
5. **If successful**:
- Write full migration script
- Test on copy of production ledger
- Validate balances match
- Schedule migration (maintenance window)
- Update balance query methods
- Deploy and monitor
6. **If unsuccessful**:
- Document why price notation doesn't work
- Consider Beancount plugin approach
- Or accept manual aggregation with caching
---
**Document Status**: Awaiting test results
**Next Action**: Run test scripts and report findings

View file

@ -0,0 +1,386 @@
# SATS-Equivalent Metadata Field
**Date**: 2025-01-12
**Status**: Current Architecture
**Location**: Beancount posting metadata
---
## Overview
The `sats-equivalent` metadata field is Castle's solution for **dual-currency tracking** in a fiat-denominated ledger. It preserves Bitcoin (satoshi) amounts alongside fiat currency amounts without violating accounting principles or creating multi-currency complexity in position balances.
### Quick Summary
- **Purpose**: Track Bitcoin/Lightning amounts in a EUR-denominated ledger
- **Location**: Beancount posting metadata (not position amounts)
- **Format**: String containing absolute satoshi amount (e.g., `"337096"`)
- **Primary Use**: Calculate user balances in satoshis (Castle's primary currency)
- **Key Principle**: Satoshis are for reference; EUR is the actual transaction currency
---
## The Problem: Dual-Currency Tracking
Castle needs to track both:
1. **Fiat amounts** (EUR, USD) - The actual transaction currency
2. **Bitcoin amounts** (satoshis) - The Lightning Network settlement currency
### Why Not Just Use SATS as Position Amounts?
**Accounting Reality**: When a user pays €36.93 cash for groceries, the transaction is denominated in EUR, not Bitcoin. Recording it as Bitcoin would:
- ❌ Misrepresent the actual transaction
- ❌ Create exchange rate volatility issues
- ❌ Complicate traditional accounting reconciliation
- ❌ Make fiat-based reporting difficult
**Castle's Philosophy**: Record transactions in their **actual currency**, with Bitcoin as supplementary data.
---
## Architecture: EUR-Primary Format
### Current Ledger Format
```beancount
2025-11-10 * "Groceries (36.93 EUR)" #expense-entry
Expenses:Food:Supplies 36.93 EUR
sats-equivalent: "39669"
reference: "cash-payment-abc123"
Liabilities:Payable:User-5987ae95 -36.93 EUR
sats-equivalent: "39669"
```
**Key Components:**
- **Position Amount**: `36.93 EUR` - The actual transaction amount
- **Metadata**: `sats-equivalent: "39669"` - The Bitcoin equivalent at time of transaction
- **Sign**: The sign (debit/credit) is on the EUR amount; sats-equivalent is always absolute value
### How It's Created
In `views_api.py:839`:
```python
# If fiat currency is provided, use EUR-based format
if fiat_currency and fiat_amount:
# EUR-based posting (current architecture)
posting_metadata["sats-equivalent"] = str(abs(line.amount))
# Apply the sign from line.amount to fiat_amount
signed_fiat_amount = fiat_amount if line.amount >= 0 else -fiat_amount
posting = {
"account": account.name,
"amount": f"{signed_fiat_amount:.2f} {fiat_currency}",
"meta": posting_metadata if posting_metadata else None
}
```
**Critical Details:**
- `line.amount` is always in satoshis internally
- The sign (debit/credit) transfers to the fiat amount
- `sats-equivalent` stores the **absolute value** of the satoshi amount
- Sign interpretation depends on account type (Asset/Liability/etc.)
---
## Usage: Balance Calculation
### Primary Use Case: User Balances
Castle's core function is tracking **who owes whom** in satoshis. The `sats-equivalent` metadata enables this.
**Flow** (`fava_client.py:220-248`):
```python
# Parse posting amount (EUR/USD)
fiat_match = re.match(r'^(-?[\d.]+)\s+([A-Z]{3})$', amount_str)
if fiat_match and fiat_match.group(2) in ('EUR', 'USD', 'GBP'):
fiat_amount = Decimal(fiat_match.group(1))
fiat_currency = fiat_match.group(2)
# Track fiat balance
fiat_balances[fiat_currency] += fiat_amount
# Extract SATS equivalent from metadata
posting_meta = posting.get("meta", {})
sats_equiv = posting_meta.get("sats-equivalent")
if sats_equiv:
# Apply the sign from fiat_amount to sats_equiv
sats_amount = int(sats_equiv) if fiat_amount > 0 else -int(sats_equiv)
total_sats += sats_amount
```
**Sign Interpretation:**
- EUR amount is `36.93` (positive/debit) → sats is `+39669`
- EUR amount is `-36.93` (negative/credit) → sats is `-39669`
### Secondary Use: Journal Entry Display
When displaying transactions to users (`views_api.py:747-751`):
```python
# Extract sats equivalent from metadata
posting_meta = first_posting.get("meta", {})
sats_equiv = posting_meta.get("sats-equivalent")
if sats_equiv:
amount_sats = abs(int(sats_equiv))
```
This allows the UI to show both EUR and SATS amounts for each transaction.
---
## Why Metadata Instead of Positions?
### The BQL Limitation
Beancount Query Language (BQL) **cannot access metadata**. This means:
```sql
-- ✅ This works (queries position amounts):
SELECT account, sum(position) WHERE account ~ 'User-5987ae95'
-- Returns: EUR positions (not useful for satoshi balances)
-- ❌ This is NOT possible:
SELECT account, sum(meta["sats-equivalent"]) WHERE account ~ 'User-5987ae95'
-- Error: BQL cannot access metadata
```
### Why Castle Accepts This Trade-off
**Performance Analysis** (see `docs/BQL-BALANCE-QUERIES.md`):
1. **Caching solves the bottleneck**: 60-80% performance improvement from caching account/permission lookups
2. **Iteration is necessary anyway**: Even with BQL, we'd need to iterate postings to access metadata
3. **Manual aggregation is fast**: The actual summation is not the bottleneck
4. **Database queries are the bottleneck**: Solved by Phase 1 caching, not BQL
**Architectural Correctness > Query Performance**:
- ✅ Transactions recorded in their actual currency
- ✅ No artificial multi-currency positions
- ✅ Clean accounting reconciliation
- ✅ Exchange rate changes don't affect historical records
---
## Alternative Considered: Price Notation
### Price Notation Format (Not Implemented)
```beancount
2025-11-10 * "Groceries"
Expenses:Food -360.00 EUR @@ 337096 SATS
Liabilities:Payable:User-abc 360.00 EUR @@ 337096 SATS
```
**Pros:**
- ✅ BQL can query prices (enables BQL aggregation)
- ✅ Standard Beancount syntax
- ✅ SATS trackable via price database
**Cons:**
- ❌ Semantically incorrect: `@@` means "total price paid", not "equivalent value"
- ❌ Implies currency conversion happened (it didn't)
- ❌ Confuses future readers about transaction nature
- ❌ Complicates Beancount's price database
**Decision**: Metadata is more semantically correct for "reference value" than price notation.
See `docs/BQL-PRICE-NOTATION-SOLUTION.md` for full analysis.
---
## Data Flow Example
### User Adds Expense
**User Action**: "I paid €36.93 cash for groceries"
**Castle's Internal Representation**:
```python
# User provides or Castle calculates:
fiat_amount = Decimal("36.93") # EUR
fiat_currency = "EUR"
amount_sats = 39669 # Calculated from exchange rate
# Create journal entry line:
line = CreateEntryLine(
account_id=expense_account.id,
amount=amount_sats, # Internal: always satoshis
metadata={
"fiat_currency": "EUR",
"fiat_amount": "36.93"
}
)
```
**Beancount Entry Created** (`views_api.py:835-849`):
```beancount
2025-11-10 * "Groceries (36.93 EUR)" #expense-entry
Expenses:Food:Supplies 36.93 EUR
sats-equivalent: "39669"
Liabilities:Payable:User-5987ae95 -36.93 EUR
sats-equivalent: "39669"
```
**Balance Calculation** (`fava_client.py:get_user_balance`):
```python
# Iterate all postings for user accounts
# For each posting:
# - Parse EUR amount: -36.93 EUR (credit to liability)
# - Extract sats-equivalent: "39669"
# - Apply sign: -36.93 is negative → sats = -39669
# - Accumulate: user_balance_sats += -39669
# Result: negative balance = Castle owes user
```
**User Balance Response**:
```json
{
"user_id": "5987ae95",
"balance": -39669, // Castle owes user 39,669 sats
"fiat_balances": {
"EUR": "-36.93" // Castle owes user €36.93
}
}
```
---
## Implementation Details
### Where It's Set
**Primary Location**: `views_api.py:835-849` (Creating journal entries)
All EUR-based postings get `sats-equivalent` metadata:
- Expense entries (user adds liability)
- Receivable entries (admin records what user owes)
- Revenue entries (direct income)
- Payment entries (settling balances)
### Where It's Read
**Primary Location**: `fava_client.py:239-247` (Balance calculation)
Used in:
1. `get_user_balance()` - Calculate individual user balance
2. `get_all_user_balances()` - Calculate all user balances
3. `get_journal_entries()` - Display transaction amounts
### Data Type and Format
- **Type**: String (Beancount metadata values must be strings or numbers)
- **Format**: Absolute value, no sign, no decimal point
- **Examples**:
- ✅ `"39669"` (correct)
- ✅ `"1000000"` (1M sats)
- ❌ `"-39669"` (incorrect: sign goes on EUR amount)
- ❌ `"396.69"` (incorrect: satoshis are integers)
---
## Key Principles
### 1. Record in Transaction Currency
```beancount
# ✅ CORRECT: User paid EUR, record in EUR
Expenses:Food 36.93 EUR
sats-equivalent: "39669"
# ❌ WRONG: Recording Bitcoin when user paid cash
Expenses:Food 39669 SATS {36.93 EUR}
```
### 2. Preserve Historical Values
The `sats-equivalent` is the **exact satoshi amount at transaction time**. It does NOT change when exchange rates change.
**Example:**
- 2025-11-10: User pays €36.93 → 39,669 sats (rate: 1074.19 sats/EUR)
- 2025-11-15: Exchange rate changes to 1100 sats/EUR
- **Metadata stays**: `sats-equivalent: "39669"`
- **If we used current rate**: Would become 40,623 sats ❌
### 3. Separate Fiat and Sats Balances
Castle tracks TWO independent balances:
- **Satoshi balance**: Sum of `sats-equivalent` metadata (primary)
- **Fiat balances**: Sum of EUR/USD position amounts (secondary)
These are calculated independently and don't cross-convert.
### 4. Absolute Values in Metadata
The sign (debit/credit) lives on the position amount, NOT the metadata.
```beancount
# Debit (expense increases):
Expenses:Food 36.93 EUR # Positive
sats-equivalent: "39669" # Absolute value
# Credit (liability increases):
Liabilities:Payable -36.93 EUR # Negative
sats-equivalent: "39669" # Same absolute value
```
---
## Migration Path
### Future: If We Change to SATS-Primary Format
**Hypothetical future format:**
```beancount
; SATS as position, EUR as cost:
2025-11-10 * "Groceries"
Expenses:Food 39669 SATS {36.93 EUR}
Liabilities:Payable:User-abc -39669 SATS {36.93 EUR}
```
**Benefits:**
- ✅ BQL can query SATS directly
- ✅ No metadata parsing needed
- ✅ Standard Beancount cost accounting
**Migration Script** (conceptual):
```python
# Read all postings with sats-equivalent metadata
# For each posting:
# - Extract sats from metadata
# - Extract EUR from position
# - Rewrite as: "<sats> SATS {<eur> EUR}"
```
**Decision**: Not implementing now because:
1. Current architecture is semantically correct
2. Performance is acceptable with caching
3. Migration would break existing tooling
4. EUR-primary aligns with accounting reality
---
## Related Documentation
- `docs/BQL-BALANCE-QUERIES.md` - Why BQL can't query metadata and performance analysis
- `docs/BQL-PRICE-NOTATION-SOLUTION.md` - Alternative using price notation (not implemented)
- `beancount_format.py` - Functions that create entries with sats-equivalent metadata
- `fava_client.py:get_user_balance()` - How metadata is parsed for balance calculation
---
## Technical Summary
**Field**: `sats-equivalent`
**Type**: Metadata (string)
**Location**: Beancount posting metadata
**Format**: Absolute satoshi amount as string (e.g., `"39669"`)
**Purpose**: Track Bitcoin equivalent of fiat transactions
**Primary Use**: Calculate user satoshi balances
**Sign Handling**: Inherits from position amount (EUR/USD)
**Queryable via BQL**: ❌ No (BQL cannot access metadata)
**Performance**: ✅ Acceptable with caching (60-80% improvement)
**Architectural Status**: ✅ Current production format
**Future Migration**: Possible to SATS-primary if needed