Design: choose unit-of-account convention (fiat-first vs bitcoin-first) and migration path #18

Open
opened 2026-05-17 17:54:35 +00:00 by padreug · 0 comments
Owner

Summary

Libra today records postings as fiat-denominated with @@ SATS price notation — fiat is the unit of account, SATS is metadata about the BTC/fiat rate at posting time. The long-term thesis is bitcoin-first accounting (SATS as unit of account, fiat as cost basis). This issue tracks the decision and the eventual migration.

The trade-off surfaces every time the BTC/fiat rate moves between accrual and settlement (or between any two postings on the same lot). See #16's comment thread for a worked example: a $200 debt booked at 200,000 sats and settled later for $200 = 250,000 sats — the 50k sats delta has to land somewhere, and where depends on this convention.

Today: fiat-first (@@ price notation)

2026-01-01 ! "Debt accrual"
  Assets:Receivable:User-A    200.00 USD @@ 200000 SATS
  Income:Sales               -200.00 USD @@ 200000 SATS
  • Holding unit: USD. SATS is per-posting metadata.
  • Beancount sums balances in USD; sats deltas across time vanish.
  • Pros: stable user-facing balances ("you owe $200" stays $200); readable by non-BTC stakeholders; cross-user fiat settlement is clean (no FX gain account needed).
  • Cons: misaligned with the bitcoin-first thesis; BTC volatility is invisible in the ledger and only recoverable from price metadata.

Encoded in beancount_format.pyformat_expense_entry, format_income_entry, format_revenue_entry, etc. all emit <fiat> <CCY> @@ <sats> SATS.

Target: bitcoin-first ({} cost basis)

2026-01-01 ! "Debt accrual"
  Assets:Receivable:User-A    200000 SATS {0.00100000 USD}
  Income:Sales               -200000 SATS {0.00100000 USD}

2026-02-01 * "Settle User A debt"
  Assets:Cash                 250000 SATS @@ 200.00 USD
  Assets:Receivable:User-A   -200000 SATS {} @@ 200.00 USD
  Income:PnL                                                  ; auto-balances 50000 SATS gain
  • Holding unit: SATS. USD on {} is the per-lot cost basis.
  • Reduction posts at the lot's recorded cost; Beancount's PNL plugin routes the realized gain/loss to a configured account.
  • Pros: aligns with the bitcoin-first thesis; gains/losses on BTC-denominated debts are explicit and queryable; cross-user/cross-time settlement gets first-class accounting.
  • Cons: balances move with BTC price (cognitive load for users); needs Income:PnL (or equivalent) account in the default chart; every existing entry's notation needs migration; UI has to communicate fluctuating sats balances and explain realized gain/loss to non-bitcoiners.

Canonical reference: Beancount's trading_with_beancount doc (originally for stocks, applies cleanly to BTC). Community "bitcoin-only" Beancount setups follow this pattern.

Hybrid (transitional)

Keep day-to-day postings fiat-first today, but emit Beancount price directives for BTC/ on every posting date. Doesn't change the unit of account; lets a separate report derive "what would this look like under bitcoin-first?" The fiat-first ledger stays the source of truth.

Useful as: (a) a low-risk way to expose the bitcoin-first perspective in the UI without breaking the ledger; (b) a discovery phase before committing to a full migration; (c) a permanent option if the user base never tolerates fluctuating sats balances.

What to decide

  1. Convention — stay fiat-first indefinitely, move to bitcoin-first on a known timeline, or commit to the hybrid as the long-term shape?
  2. Trigger — if not "now," what concrete event would force the call? E.g., first cross-user settlement (#16), first audit complaint about hidden FX gains, an explicit org policy shift toward sats-denominated wages/dues.
  3. Migration scope (if flipping) — re-emit existing @@ entries as {}? Keep history as-is and only flip new entries? What does Fava show for the seam?
  4. UI implications — how do we show a "you hold 200,000 sats of receivable" balance to a member who thinks in EUR? Show both? Toggle?
  5. PnL account placementIncome:PnL vs Income:Trading:BTC vs a separate Equity:UnrealizedGains for unrealized; needs to land in the default chart of accounts.
  6. Backwards compatibility — existing tooling (the Outstanding Balances card, Fava queries in fava_client.py, the webapp Transaction History) assumes fiat-denominated balances; a flip needs each touched.

Why this matters

  • #16 (cross-user receivable settlement) is the first feature that visibly forces the choice; deferring is fine, but #16's implementation will pick a convention by accident if we don't decide deliberately.
  • Every new accounting feature (budgets #10, refunds #2, attachments #11) inherits today's convention by default. The longer we wait, the more entries get re-encoded in a flip.

Not blocking

This is a design issue, not implementation. No code change is associated. Cross-link from any feature that meaningfully depends on the answer.

## Summary Libra today records postings as **fiat-denominated with `@@ SATS` price notation** — fiat is the unit of account, SATS is metadata about the BTC/fiat rate at posting time. The long-term thesis is bitcoin-first accounting (SATS as unit of account, fiat as cost basis). This issue tracks the decision and the eventual migration. The trade-off surfaces every time the BTC/fiat rate moves between accrual and settlement (or between any two postings on the same lot). See [#16's comment thread](https://git.atitlan.io/aiolabs/libra/issues/16#issuecomment-724) for a worked example: a $200 debt booked at 200,000 sats and settled later for $200 = 250,000 sats — the 50k sats delta has to land somewhere, and where depends on this convention. ## Today: fiat-first (`@@` price notation) ```beancount 2026-01-01 ! "Debt accrual" Assets:Receivable:User-A 200.00 USD @@ 200000 SATS Income:Sales -200.00 USD @@ 200000 SATS ``` - Holding unit: USD. SATS is per-posting metadata. - Beancount sums balances in USD; sats deltas across time vanish. - Pros: stable user-facing balances ("you owe $200" stays $200); readable by non-BTC stakeholders; cross-user fiat settlement is clean (no FX gain account needed). - Cons: misaligned with the bitcoin-first thesis; BTC volatility is invisible in the ledger and only recoverable from price metadata. Encoded in `beancount_format.py` — `format_expense_entry`, `format_income_entry`, `format_revenue_entry`, etc. all emit `<fiat> <CCY> @@ <sats> SATS`. ## Target: bitcoin-first (`{}` cost basis) ```beancount 2026-01-01 ! "Debt accrual" Assets:Receivable:User-A 200000 SATS {0.00100000 USD} Income:Sales -200000 SATS {0.00100000 USD} 2026-02-01 * "Settle User A debt" Assets:Cash 250000 SATS @@ 200.00 USD Assets:Receivable:User-A -200000 SATS {} @@ 200.00 USD Income:PnL ; auto-balances 50000 SATS gain ``` - Holding unit: SATS. USD on `{}` is the per-lot cost basis. - Reduction posts at the lot's recorded cost; Beancount's PNL plugin routes the realized gain/loss to a configured account. - Pros: aligns with the bitcoin-first thesis; gains/losses on BTC-denominated debts are explicit and queryable; cross-user/cross-time settlement gets first-class accounting. - Cons: balances move with BTC price (cognitive load for users); needs `Income:PnL` (or equivalent) account in the default chart; every existing entry's notation needs migration; UI has to communicate fluctuating sats balances and explain realized gain/loss to non-bitcoiners. Canonical reference: Beancount's [trading_with_beancount](https://beancount.github.io/docs/trading_with_beancount.html) doc (originally for stocks, applies cleanly to BTC). Community "bitcoin-only" Beancount setups follow this pattern. ## Hybrid (transitional) Keep day-to-day postings fiat-first today, but emit Beancount `price` directives for BTC/<fiat> on every posting date. Doesn't change the unit of account; lets a separate report derive "what would this look like under bitcoin-first?" The fiat-first ledger stays the source of truth. Useful as: (a) a low-risk way to expose the bitcoin-first perspective in the UI without breaking the ledger; (b) a discovery phase before committing to a full migration; (c) a permanent option if the user base never tolerates fluctuating sats balances. ## What to decide 1. **Convention** — stay fiat-first indefinitely, move to bitcoin-first on a known timeline, or commit to the hybrid as the long-term shape? 2. **Trigger** — if not "now," what concrete event would force the call? E.g., first cross-user settlement (#16), first audit complaint about hidden FX gains, an explicit org policy shift toward sats-denominated wages/dues. 3. **Migration scope (if flipping)** — re-emit existing `@@` entries as `{}`? Keep history as-is and only flip new entries? What does Fava show for the seam? 4. **UI implications** — how do we show a "you hold 200,000 sats of receivable" balance to a member who thinks in EUR? Show both? Toggle? 5. **PnL account placement** — `Income:PnL` vs `Income:Trading:BTC` vs a separate `Equity:UnrealizedGains` for unrealized; needs to land in the default chart of accounts. 6. **Backwards compatibility** — existing tooling (the Outstanding Balances card, Fava queries in `fava_client.py`, the webapp Transaction History) assumes fiat-denominated balances; a flip needs each touched. ## Why this matters - #16 (cross-user receivable settlement) is the first feature that visibly forces the choice; deferring is fine, but #16's implementation will pick a convention by accident if we don't decide deliberately. - Every new accounting feature (budgets #10, refunds #2, attachments #11) inherits today's convention by default. The longer we wait, the more entries get re-encoded in a flip. ## Not blocking This is a design issue, not implementation. No code change is associated. Cross-link from any feature that meaningfully depends on the answer.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/libra#18
No description provided.