docs: update functional identifier refs to spirekeeper
Some checks failed
ci.yml / docs: update functional identifier refs to spirekeeper (push) Failing after 0s

Runbook SQL (spirekeeper.dca_*), ext URL paths, code-location paths,
and the DB-schema name in docs/CLAUDE/README move to the new identity.
Rewrites the placeholder description.md with a real one. Historical
aiolabs/satmachineadmin#N issue/repo links stay pointing at the
original repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-06-13 22:31:21 +02:00
commit 4ac640a499
6 changed files with 142 additions and 143 deletions

View file

@ -213,7 +213,7 @@ commission_amount = 266800 - 258835 = 7,965 sats (to commission wallet)
### Security Considerations ### Security Considerations
- **Superuser Authentication**: Admin extension requires LNBits superuser login - **Superuser Authentication**: Admin extension requires LNBits superuser login
- **Wallet Admin Keys**: Client extension uses wallet admin keys for user operations - **Wallet Admin Keys**: Client extension uses wallet admin keys for user operations
- **Database Access**: Only superusers can write to satoshimachine database - **Database Access**: Only superusers can write to spirekeeper database
- SSH tunnel encryption for database connectivity - SSH tunnel encryption for database connectivity
- Read-only database permissions for Lamassu access - Read-only database permissions for Lamassu access
- Input sanitization and type validation - Input sanitization and type validation
@ -230,7 +230,7 @@ on existing installs:
```sql ```sql
SELECT a.id, a.username, a.pubkey, m.id, m.machine_npub SELECT a.id, a.username, a.pubkey, m.id, m.machine_npub
FROM accounts a FROM accounts a
JOIN ext_satoshimachine.dca_machines m JOIN ext_spirekeeper.dca_machines m
ON LOWER(a.pubkey) = LOWER(m.machine_npub); ON LOWER(a.pubkey) = LOWER(m.machine_npub);
``` ```

View file

@ -188,7 +188,7 @@ The extension creates several tables:
├── config.json # Extension configuration ├── config.json # Extension configuration
├── manifest.json # Extension manifest ├── manifest.json # Extension manifest
├── templates/ ├── templates/
│ └── satmachineadmin/ │ └── spirekeeper/
│ └── index.html # Main UI template │ └── index.html # Main UI template
└── static/ └── static/
└── js/ └── js/

View file

@ -1,10 +1,9 @@
SatMachineAdmin can be used as a template for building new extensions, it includes a bunch of functions that can be edited/deleted as you need them. spirekeeper is the operator-side administration extension for bitSpire — it manages one or more bitSpire ATMs and runs the Dollar Cost Averaging (DCA) distribution that pays registered clients out of their confirmed deposit balances.
This is a longform description that will be used in the advanced description when users click on the "more" button on the extension cards. It listens for settlement events published by each ATM over Nostr (kind-21000, NIP-44 v2 encrypted), pays the resulting invoices through LNbits, and splits the proceeds across the principal, operator-fee, and commission legs according to the per-machine and super-config fee settings.
Adding some bullets is nice covering: - **ATM fleet management** — register and configure each bitSpire by its machine npub; publish fee config and cassette state over Nostr.
- **DCA distribution** — proportional ("flow") allocation across clients based on their remaining deposit balances, with a full per-leg audit trail.
- **Operator administration** — deposit confirmation workflow, client balance tracking, settlement history with drill-down, and CSV export for accounting.
- Functionality Requires LNbits superuser access for administration. The Lightning ATM side is served by the bitSpire device; this extension is the back office.
- Use cases
...and some other text about just how great this etension is.

View file

@ -7,7 +7,7 @@
## 0 · Why this document exists ## 0 · Why this document exists
Today the satoshimachine code lives at `~/dev/shared/extensions/satmachineadmin` on branch `v2-bitspire`. v2 swapped the legacy Lamassu SSH/PostgreSQL polling model for a Nostrnative one: bitSpire publishes invoices over kind21000 NIP44 v2 events, LNbits pays them, and our extension hooks the resulting `Payment` object. Today the satoshimachine code lives at `~/dev/shared/extensions/spirekeeper` on branch `v2-bitspire`. v2 swapped the legacy Lamassu SSH/PostgreSQL polling model for a Nostrnative one: bitSpire publishes invoices over kind21000 NIP44 v2 events, LNbits pays them, and our extension hooks the resulting `Payment` object.
The hard truth: the *settlement* itself uses Lightning (so it can't be forged once a preimage lands), but everything *around* the settlement — who the ATM is, what operator it belongs to, what the principal/commission split was, and what fiat was dispensed — currently rides on **mutable, unauthenticated metadata** (`Payment.extra`) plus a **stopgap that has the ATM hold the operator's own Nostr private key**. The latter means physical possession of the ATM = total compromise of the operator's LNbits account. The hard truth: the *settlement* itself uses Lightning (so it can't be forged once a preimage lands), but everything *around* the settlement — who the ATM is, what operator it belongs to, what the principal/commission split was, and what fiat was dispensed — currently rides on **mutable, unauthenticated metadata** (`Payment.extra`) plus a **stopgap that has the ATM hold the operator's own Nostr private key**. The latter means physical possession of the ATM = total compromise of the operator's LNbits account.
@ -66,7 +66,7 @@ Lamassu's old answer here was TLS cert pinning. We have a richer toolbox — Nos
register_invoice_listener fires register_invoice_listener fires
satmachineadmin/tasks.py:_handle_payment spirekeeper/tasks.py:_handle_payment
┌─────────────────────────────────┴────────────────────────────┐ ┌─────────────────────────────────┴────────────────────────────┐
▼ ▼ ▼ ▼
@ -144,7 +144,7 @@ T3, T5, T6 are the ones that keep the hardware honest. T3 + T6 are *the* reason
## 4 · Audit findings — current state inventory ## 4 · Audit findings — current state inventory
Pulled from the two recent codelevel audits of `~/dev/shared/extensions/satmachineadmin` (operatorscoping inventory) and `~/dev/lnbits/nostr-transport` (transport primitives). Pulled from the two recent codelevel audits of `~/dev/shared/extensions/spirekeeper` (operatorscoping inventory) and `~/dev/lnbits/nostr-transport` (transport primitives).
### 4.1 What's already strong ### 4.1 What's already strong
@ -307,7 +307,7 @@ None of those need to change. The new layers slot in *above* them.
| **S4 — NIP78 permachine config + fleet roster** | Operator publishes `kind:30078` config + `kind:30000` fleet list. Handler crosschecks ATM npub ∈ fleet; reads maxwithdraw/fee policy from config. | G1, G9 | 1 week | Define config schema; backwardscompat path for preNIP78 machines. | | **S4 — NIP78 permachine config + fleet roster** | Operator publishes `kind:30078` config + `kind:30000` fleet list. Handler crosschecks ATM npub ∈ fleet; reads maxwithdraw/fee policy from config. | G1, G9 | 1 week | Define config schema; backwardscompat path for preNIP78 machines. |
| **S5 — `sender_pubkey` persistence + signed metadata in Payment.extra** | When the dispatcher writes a Payment row, it stamps `Payment.extra.sender_pubkey`, `delegation_root`, and an HMAC over the key fields keyed by the LNbits server's own secret. Mutation postwrite breaks the HMAC. | G2 (DBside), G5, G6 | 35 days | LNbits PR — fairly localised. | | **S5 — `sender_pubkey` persistence + signed metadata in Payment.extra** | When the dispatcher writes a Payment row, it stamps `Payment.extra.sender_pubkey`, `delegation_root`, and an HMAC over the key fields keyed by the LNbits server's own secret. Mutation postwrite breaks the HMAC. | G2 (DBside), G5, G6 | 35 days | LNbits PR — fairly localised. |
| **S6 — Rate limiting + rostergated autoaccount** | Autoaccountfromnpub only fires if the npub appears in some operator's NIP78 fleet OR if an explicit "open enrollment" flag is set. Relay/handlerlevel rate limit per pubkey. | G8, G9 | 1 week | LNbits PR. | | **S6 — Rate limiting + rostergated autoaccount** | Autoaccountfromnpub only fires if the npub appears in some operator's NIP78 fleet OR if an explicit "open enrollment" flag is set. Relay/handlerlevel rate limit per pubkey. | G8, G9 | 1 week | LNbits PR. |
| **S7 — NIP46 bunker option** | Operator can pair satmachineadmin with a Bunker (Amber, Nunchuk Custody, etc.). Operator's nsec leaves LNbits' DB; LNbits stores only the bunker connection. | G6, partial G5 | 46 weeks | Largest. Defer until S0S5 land. | | **S7 — NIP46 bunker option** | Operator can pair spirekeeper with a Bunker (Amber, Nunchuk Custody, etc.). Operator's nsec leaves LNbits' DB; LNbits stores only the bunker connection. | G6, partial G5 | 46 weeks | Largest. Defer until S0S5 land. |
| **S8 — Cashin path** | Wire `is_out=True` cashin handling: LNURLwithdraw with expiration matching the kind21000 invoice TTL, attestation receipt on settle, refund queue for stale links. | G10 | 2 weeks | Out of scope for this security doc but tracked here for completeness. | | **S8 — Cashin path** | Wire `is_out=True` cashin handling: LNURLwithdraw with expiration matching the kind21000 invoice TTL, attestation receipt on settle, refund queue for stale links. | G10 | 2 weeks | Out of scope for this security doc but tracked here for completeness. |
Recommended sequencing for the *next sprint*: **S0 + S1 + S5**. They give us the biggest security delta with no upstream LNbits dependency for S0/S1 and a small, wellscoped LNbits patch for S5. S2/S3/S4 are the proper Nostrnative layer and should land in the sprint after. Recommended sequencing for the *next sprint*: **S0 + S1 + S5**. They give us the biggest security delta with no upstream LNbits dependency for S0/S1 and a small, wellscoped LNbits patch for S5. S2/S3/S4 are the proper Nostrnative layer and should land in the sprint after.
@ -359,12 +359,12 @@ For an auditor or new contributor doing a walkthrough:
| File | Role | Note | | File | Role | Note |
|---|---|---| |---|---|---|
| `~/dev/shared/extensions/satmachineadmin/tasks.py` | LNbits invoice listener. Entry point for all settlements today. | `_handle_payment:56-95` — loadbearing routing. | | `~/dev/shared/extensions/spirekeeper/tasks.py` | LNbits invoice listener. Entry point for all settlements today. | `_handle_payment:56-95` — loadbearing routing. |
| `~/dev/shared/extensions/satmachineadmin/bitspire.py` | Parses Payment.extra. The trust boundary. | `parse_settlement:68-92` — happy vs fallback path. | | `~/dev/shared/extensions/spirekeeper/bitspire.py` | Parses Payment.extra. The trust boundary. | `parse_settlement:68-92` — happy vs fallback path. |
| `~/dev/shared/extensions/satmachineadmin/distribution.py` | Threeleg distribution chain. | `process_settlement` — uses claim pattern. | | `~/dev/shared/extensions/spirekeeper/distribution.py` | Threeleg distribution chain. | `process_settlement` — uses claim pattern. |
| `~/dev/shared/extensions/satmachineadmin/crud.py` | Operatorscoped DB layer. | `claim_settlement_for_processing`, `_machine_owned_by`. | | `~/dev/shared/extensions/spirekeeper/crud.py` | Operatorscoped DB layer. | `claim_settlement_for_processing`, `_machine_owned_by`. |
| `~/dev/shared/extensions/satmachineadmin/views_api.py` | 33 routes, all `check_user_exists` except superconfig PUT. | `_assert_wallet_owned_by` is the walletIDOR fix. | | `~/dev/shared/extensions/spirekeeper/views_api.py` | 33 routes, all `check_user_exists` except superconfig PUT. | `_assert_wallet_owned_by` is the walletIDOR fix. |
| `~/dev/shared/extensions/satmachineadmin/migrations.py` | Schema. | `dca_settlements` is the audit row; `dca_payments` is the leg row. | | `~/dev/shared/extensions/spirekeeper/migrations.py` | Schema. | `dca_settlements` is the audit row; `dca_payments` is the leg row. |
| `~/dev/shocknet/lamassu-next/deploy/nixos/provision-atm.sh` | Where keys land on the ATM today. | `:81-99``VITE_ATM_PRIVATE_KEY` and the Option1 stopgap. | | `~/dev/shocknet/lamassu-next/deploy/nixos/provision-atm.sh` | Where keys land on the ATM today. | `:81-99``VITE_ATM_PRIVATE_KEY` and the Option1 stopgap. |
| `~/dev/lnbits/nostr-transport/lnbits/core/services/nostr_transport/` | LNbits transport handler (upstream we depend on). | NIP44 v2 crypto here; G5/G6/G7 fixes will live here. | | `~/dev/lnbits/nostr-transport/lnbits/core/services/nostr_transport/` | LNbits transport handler (upstream we depend on). | NIP44 v2 crypto here; G5/G6/G7 fixes will live here. |
| `~/dev/nostr-protocol/nips/26.md` | Delegation. | Source for S2. | | `~/dev/nostr-protocol/nips/26.md` | Delegation. | Source for S2. |
@ -397,7 +397,7 @@ How we'd test the proposed design endtoend, once S0S5 land:
Once approved: Once approved:
1. The PDF for printing will be generated postplanmode (requires shell exec). Recommended path: render the markdown via `pandoc` to `~/dev/shared/extensions/satmachineadmin/docs/security-pathway-v1.pdf`; the markdown source will live at `~/dev/shared/extensions/satmachineadmin/docs/security-pathway-v1.md` so future contributors edit it inrepo. 1. The PDF for printing will be generated postplanmode (requires shell exec). Recommended path: render the markdown via `pandoc` to `~/dev/shared/extensions/spirekeeper/docs/security-pathway-v1.pdf`; the markdown source will live at `~/dev/shared/extensions/spirekeeper/docs/security-pathway-v1.md` so future contributors edit it inrepo.
2. Open Forgejo epics on `aiolabs/satmachineadmin` linking back to existing `#9/#11/#12` and adding a new one for "Security pathway hardening (S0S7)." 2. Open Forgejo epics on `aiolabs/satmachineadmin` linking back to existing `#9/#11/#12` and adding a new one for "Security pathway hardening (S0S7)."
3. Open a tracking issue on `aiolabs/lnbits` against the `nostr-transport` branch for the LNbitsside primitives (S2, S5, S6). 3. Open a tracking issue on `aiolabs/lnbits` against the `nostr-transport` branch for the LNbitsside primitives (S2, S5, S6).
4. Sequence sprint: **S0 + S1 + S5 first** (highest ratio of security delta to upstream coupling). S2/S3/S4 in the following sprint. 4. Sequence sprint: **S0 + S1 + S5 first** (highest ratio of security delta to upstream coupling). S2/S3/S4 in the following sprint.

View file

@ -38,7 +38,7 @@
-- Find duplicate transactions -- Find duplicate transactions
SELECT transaction_id, COUNT(*) as count, SELECT transaction_id, COUNT(*) as count,
STRING_AGG(id::text, ', ') as record_ids STRING_AGG(id::text, ', ') as record_ids
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
GROUP BY transaction_id GROUP BY transaction_id
HAVING COUNT(*) > 1; HAVING COUNT(*) > 1;
``` ```
@ -60,16 +60,16 @@ HAVING COUNT(*) > 1;
-- Step 1: Identify duplicate distributions -- Step 1: Identify duplicate distributions
SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount, SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount,
COUNT(dp.id) as distribution_count COUNT(dp.id) as distribution_count
FROM satoshimachine.lamassu_transactions lt FROM spirekeeper.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
GROUP BY lt.id GROUP BY lt.id
HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM satoshimachine.dca_clients WHERE remaining_balance > 0); HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM spirekeeper.dca_clients WHERE remaining_balance > 0);
-- Step 2: Calculate over-distributed amounts per client -- Step 2: Calculate over-distributed amounts per client
SELECT client_id, SELECT client_id,
SUM(amount_sats) as total_received, SUM(amount_sats) as total_received,
-- Manual calculation of expected amount needed here -- Manual calculation of expected amount needed here
FROM satoshimachine.dca_payments FROM spirekeeper.dca_payments
WHERE lamassu_transaction_id IN (SELECT id FROM duplicates_table) WHERE lamassu_transaction_id IN (SELECT id FROM duplicates_table)
GROUP BY client_id; GROUP BY client_id;
``` ```
@ -94,7 +94,7 @@ if existing:
**Required Database Change**: **Required Database Change**:
```sql ```sql
ALTER TABLE satoshimachine.lamassu_transactions ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
``` ```
@ -117,7 +117,7 @@ ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Check last successful poll -- Check last successful poll
SELECT MAX(created_at) as last_transaction, SELECT MAX(created_at) as last_transaction,
EXTRACT(EPOCH FROM (NOW() - MAX(created_at)))/3600 as hours_since_last EXTRACT(EPOCH FROM (NOW() - MAX(created_at)))/3600 as hours_since_last
FROM satoshimachine.lamassu_transactions; FROM spirekeeper.lamassu_transactions;
-- If hours_since_last > 24, investigate immediately -- If hours_since_last > 24, investigate immediately
``` ```
@ -194,8 +194,8 @@ ssh-keygen -t ed25519 -f ~/.ssh/satmachine_lamassu -C "satmachine-polling"
-- Find stuck/failed payments (older than 1 hour, not completed) -- Find stuck/failed payments (older than 1 hour, not completed)
SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at, SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at,
c.username, dp.payment_hash c.username, dp.payment_hash
FROM satoshimachine.dca_payments dp FROM spirekeeper.dca_payments dp
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE dp.status != 'completed' WHERE dp.status != 'completed'
AND dp.created_at < NOW() - INTERVAL '1 hour' AND dp.created_at < NOW() - INTERVAL '1 hour'
ORDER BY dp.created_at DESC; ORDER BY dp.created_at DESC;
@ -207,7 +207,7 @@ ORDER BY dp.created_at DESC;
SELECT SELECT
SUM(commission_amount) as total_commission_expected, SUM(commission_amount) as total_commission_expected,
-- Manually check actual wallet balance in LNBits -- Manually check actual wallet balance in LNBits
FROM satoshimachine.lamassu_transactions; FROM spirekeeper.lamassu_transactions;
``` ```
#### Immediate Response #### Immediate Response
@ -258,9 +258,9 @@ SELECT
c.remaining_balance, c.remaining_balance,
COALESCE(SUM(d.amount), 0) as total_deposits, COALESCE(SUM(d.amount), 0) as total_deposits,
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments
FROM satoshimachine.dca_clients c FROM spirekeeper.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id; GROUP BY c.id;
``` ```
@ -319,9 +319,9 @@ SELECT
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_distributed, COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_distributed,
COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as calculated_balance, COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as calculated_balance,
c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0)) as discrepancy c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0)) as discrepancy
FROM satoshimachine.dca_clients c FROM spirekeeper.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id, c.username, c.remaining_balance GROUP BY c.id, c.username, c.remaining_balance
HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0))) > 1; HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0))) > 1;
``` ```
@ -341,11 +341,11 @@ HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE
```sql ```sql
-- Get complete transaction history for client -- Get complete transaction history for client
SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at
FROM satoshimachine.dca_deposits FROM spirekeeper.dca_deposits
WHERE client_id = <client_id> WHERE client_id = <client_id>
UNION ALL UNION ALL
SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL
FROM satoshimachine.dca_payments FROM spirekeeper.dca_payments
WHERE client_id = <client_id> WHERE client_id = <client_id>
ORDER BY created_at; ORDER BY created_at;
``` ```
@ -361,18 +361,18 @@ ORDER BY created_at;
**Option A: Adjustment Entry** (Recommended) **Option A: Adjustment Entry** (Recommended)
```sql ```sql
-- Create compensating deposit for positive discrepancy -- Create compensating deposit for positive discrepancy
INSERT INTO satoshimachine.dca_deposits (client_id, amount, status, note) INSERT INTO spirekeeper.dca_deposits (client_id, amount, status, note)
VALUES (<client_id>, <adjustment_amount>, 'confirmed', 'Balance correction - reconciliation 2025-10-19'); VALUES (<client_id>, <adjustment_amount>, 'confirmed', 'Balance correction - reconciliation 2025-10-19');
-- OR Create compensating payment for negative discrepancy -- OR Create compensating payment for negative discrepancy
INSERT INTO satoshimachine.dca_payments (client_id, amount_sats, status, note) INSERT INTO spirekeeper.dca_payments (client_id, amount_sats, status, note)
VALUES (<client_id>, <adjustment_amount>, 'completed', 'Balance correction - reconciliation 2025-10-19'); VALUES (<client_id>, <adjustment_amount>, 'completed', 'Balance correction - reconciliation 2025-10-19');
``` ```
**Option B: Direct Balance Update** (Use with extreme caution) **Option B: Direct Balance Update** (Use with extreme caution)
```sql ```sql
-- ONLY if audit trail is complete and discrepancy is unexplained -- ONLY if audit trail is complete and discrepancy is unexplained
UPDATE satoshimachine.dca_clients UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_balance>, SET remaining_balance = <correct_balance>,
updated_at = NOW() updated_at = NOW()
WHERE id = <client_id>; WHERE id = <client_id>;
@ -396,11 +396,11 @@ async def daily_reconciliation_check():
**Database Constraints**: **Database Constraints**:
```sql ```sql
-- Prevent negative balances -- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0); ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Prevent confirmed deposits with zero amount -- Prevent confirmed deposits with zero amount
ALTER TABLE satoshimachine.dca_deposits ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0); ADD CONSTRAINT positive_deposit CHECK (amount > 0);
``` ```
@ -435,7 +435,7 @@ SELECT
-- Calculate differences -- Calculate differences
base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as base_difference, base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as base_difference,
commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) as commission_difference commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) as commission_difference
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
WHERE ABS(base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) > 1 WHERE ABS(base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) > 1
OR ABS(commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))))) > 1; OR ABS(commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))))) > 1;
``` ```
@ -485,9 +485,9 @@ SELECT
SUM(lt.base_amount) as total_distributed, SUM(lt.base_amount) as total_distributed,
SUM(expected_base) as should_have_distributed, SUM(expected_base) as should_have_distributed,
SUM(expected_base - lt.base_amount) as client_impact SUM(expected_base - lt.base_amount) as client_impact
FROM satoshimachine.lamassu_transactions lt FROM spirekeeper.lamassu_transactions lt
JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE -- filter for affected transactions WHERE -- filter for affected transactions
GROUP BY c.id; GROUP BY c.id;
``` ```
@ -505,7 +505,7 @@ GROUP BY c.id;
- Create compensating payments to affected clients: - Create compensating payments to affected clients:
```sql ```sql
-- Add to client balances -- Add to client balances
UPDATE satoshimachine.dca_clients c UPDATE spirekeeper.dca_clients c
SET remaining_balance = remaining_balance + adjustment.amount SET remaining_balance = remaining_balance + adjustment.amount
FROM ( FROM (
-- Calculate adjustment per client -- Calculate adjustment per client
@ -572,7 +572,7 @@ assert abs(calculated_total - crypto_atoms) <= 1, "Commission calculation error
SELECT SELECT
SUM(commission_amount) as expected_total_commission, SUM(commission_amount) as expected_total_commission,
-- Compare to actual wallet balance in LNBits dashboard -- Compare to actual wallet balance in LNBits dashboard
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
WHERE created_at > '<date-of-last-known-good-balance>'; WHERE created_at > '<date-of-last-known-good-balance>';
``` ```
@ -607,7 +607,7 @@ curl -X GET https://<lnbits-host>/api/v1/wallet \
SELECT commission_wallet_id, SELECT commission_wallet_id,
LEFT(commission_wallet_adminkey, 10) || '...' as key_preview, LEFT(commission_wallet_adminkey, 10) || '...' as key_preview,
updated_at updated_at
FROM satoshimachine.lamassu_config FROM spirekeeper.lamassu_config
ORDER BY updated_at DESC ORDER BY updated_at DESC
LIMIT 1; LIMIT 1;
``` ```
@ -783,9 +783,9 @@ WITH client_financials AS (
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments, COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments,
COUNT(DISTINCT d.id) as deposit_count, COUNT(DISTINCT d.id) as deposit_count,
COUNT(DISTINCT p.id) as payment_count COUNT(DISTINCT p.id) as payment_count
FROM satoshimachine.dca_clients c FROM spirekeeper.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id GROUP BY c.id
) )
SELECT SELECT
@ -813,7 +813,7 @@ SELECT
SUM(commission_amount) as total_commission, SUM(commission_amount) as total_commission,
MIN(created_at) as first_transaction, MIN(created_at) as first_transaction,
MAX(created_at) as last_transaction MAX(created_at) as last_transaction
FROM satoshimachine.lamassu_transactions; FROM spirekeeper.lamassu_transactions;
-- 3. Failed/Pending Payments Check -- 3. Failed/Pending Payments Check
SELECT SELECT
@ -822,7 +822,7 @@ SELECT
SUM(amount_sats) as total_amount, SUM(amount_sats) as total_amount,
MIN(created_at) as oldest, MIN(created_at) as oldest,
MAX(created_at) as newest MAX(created_at) as newest
FROM satoshimachine.dca_payments FROM spirekeeper.dca_payments
GROUP BY status GROUP BY status
ORDER BY ORDER BY
CASE status CASE status
@ -839,7 +839,7 @@ SELECT
status, status,
created_at, created_at,
EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending
FROM satoshimachine.dca_deposits FROM spirekeeper.dca_deposits
WHERE status = 'pending' WHERE status = 'pending'
AND created_at < NOW() - INTERVAL '48 hours' AND created_at < NOW() - INTERVAL '48 hours'
ORDER BY created_at; ORDER BY created_at;
@ -855,7 +855,7 @@ SELECT
commission_amount, commission_amount,
ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as expected_base, ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as expected_base,
base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as difference base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as difference
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 20; LIMIT 20;
``` ```
@ -876,15 +876,15 @@ psql -h localhost -U lnbits -d lnbits
2. **Direct Configuration Update**: 2. **Direct Configuration Update**:
```sql ```sql
-- Update Lamassu config directly -- Update Lamassu config directly
UPDATE satoshimachine.lamassu_config UPDATE spirekeeper.lamassu_config
SET polling_enabled = false SET polling_enabled = false
WHERE id = (SELECT MAX(id) FROM satoshimachine.lamassu_config); WHERE id = (SELECT MAX(id) FROM spirekeeper.lamassu_config);
``` ```
3. **Manual Client Balance Update**: 3. **Manual Client Balance Update**:
```sql ```sql
-- ONLY in emergency when dashboard unavailable -- ONLY in emergency when dashboard unavailable
UPDATE satoshimachine.dca_clients UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_amount> SET remaining_balance = <correct_amount>
WHERE id = <client_id>; WHERE id = <client_id>;
-- MUST document this action in incident log -- MUST document this action in incident log
@ -910,19 +910,19 @@ uv run lnbits
```bash ```bash
# Export all DCA-related tables to CSV # Export all DCA-related tables to CSV
sqlite3 -header -csv /path/to/lnbits/database.sqlite \ sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \ "SELECT * FROM spirekeeper.lamassu_transactions;" \
> lamassu_transactions_export_$(date +%Y%m%d_%H%M%S).csv > lamassu_transactions_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \ sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \ "SELECT * FROM spirekeeper.dca_payments;" \
> dca_payments_export_$(date +%Y%m%d_%H%M%S).csv > dca_payments_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \ sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \ "SELECT * FROM spirekeeper.dca_deposits;" \
> dca_deposits_export_$(date +%Y%m%d_%H%M%S).csv > dca_deposits_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \ sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \ "SELECT * FROM spirekeeper.dca_clients;" \
> dca_clients_export_$(date +%Y%m%d_%H%M%S).csv > dca_clients_export_$(date +%Y%m%d_%H%M%S).csv
``` ```
@ -946,9 +946,9 @@ SELECT
dp.amount_sats as client_received, dp.amount_sats as client_received,
dp.status as payment_status, dp.status as payment_status,
dp.payment_hash dp.payment_hash
FROM satoshimachine.lamassu_transactions lt FROM spirekeeper.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN satoshimachine.dca_clients c ON dp.client_id = c.id LEFT JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
ORDER BY lt.created_at DESC, c.username; ORDER BY lt.created_at DESC, c.username;
``` ```
@ -981,18 +981,18 @@ async def process_lamassu_transaction(txn_data: dict) -> Optional[LamassuTransac
```sql ```sql
-- Add unique constraint on transaction_id -- Add unique constraint on transaction_id
ALTER TABLE satoshimachine.lamassu_transactions ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Prevent negative balances -- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0); ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Ensure positive amounts -- Ensure positive amounts
ALTER TABLE satoshimachine.dca_deposits ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0); ADD CONSTRAINT positive_deposit CHECK (amount > 0);
ALTER TABLE satoshimachine.dca_payments ALTER TABLE spirekeeper.dca_payments
ADD CONSTRAINT positive_payment CHECK (amount_sats > 0); ADD CONSTRAINT positive_payment CHECK (amount_sats > 0);
``` ```
@ -1196,7 +1196,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
### System Access ### System Access
**LNBits Admin Dashboard**: **LNBits Admin Dashboard**:
- URL: `https://<your-lnbits-host>/satoshimachine` - URL: `https://<your-lnbits-host>/spirekeeper`
- Requires superuser authentication - Requires superuser authentication
**Database Access**: **Database Access**:
@ -1205,7 +1205,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
sqlite3 /home/padreug/AioLabs/Git/lnbits-extensions/lnbits/data/database.sqlite sqlite3 /home/padreug/AioLabs/Git/lnbits-extensions/lnbits/data/database.sqlite
# Direct table access # Direct table access
sqlite3 /path/to/db "SELECT * FROM satoshimachine.<table_name>;" sqlite3 /path/to/db "SELECT * FROM spirekeeper.<table_name>;"
``` ```
**Log Files**: **Log Files**:
@ -1274,7 +1274,7 @@ pkill -f lnbits
```sql ```sql
-- Disable automatic polling -- Disable automatic polling
UPDATE satoshimachine.lamassu_config UPDATE spirekeeper.lamassu_config
SET polling_enabled = false; SET polling_enabled = false;
``` ```
@ -1283,7 +1283,7 @@ SET polling_enabled = false;
```sql ```sql
-- All client balances summary -- All client balances summary
SELECT id, username, remaining_balance, created_at SELECT id, username, remaining_balance, created_at
FROM satoshimachine.dca_clients FROM spirekeeper.dca_clients
ORDER BY remaining_balance DESC; ORDER BY remaining_balance DESC;
``` ```
@ -1291,7 +1291,7 @@ ORDER BY remaining_balance DESC;
```sql ```sql
SELECT id, transaction_id, created_at, crypto_atoms, base_amount, commission_amount SELECT id, transaction_id, created_at, crypto_atoms, base_amount, commission_amount
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 10; LIMIT 10;
``` ```
@ -1299,7 +1299,7 @@ LIMIT 10;
### Failed Payments ### Failed Payments
```sql ```sql
SELECT * FROM satoshimachine.dca_payments SELECT * FROM spirekeeper.dca_payments
WHERE status != 'completed' WHERE status != 'completed'
ORDER BY created_at DESC; ORDER BY created_at DESC;
``` ```
@ -1314,19 +1314,19 @@ BACKUP_DIR="emergency_backup_${DATE}"
mkdir -p $BACKUP_DIR mkdir -p $BACKUP_DIR
sqlite3 -header -csv /path/to/database.sqlite \ sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \ "SELECT * FROM spirekeeper.lamassu_transactions;" \
> ${BACKUP_DIR}/lamassu_transactions.csv > ${BACKUP_DIR}/lamassu_transactions.csv
sqlite3 -header -csv /path/to/database.sqlite \ sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \ "SELECT * FROM spirekeeper.dca_payments;" \
> ${BACKUP_DIR}/dca_payments.csv > ${BACKUP_DIR}/dca_payments.csv
sqlite3 -header -csv /path/to/database.sqlite \ sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \ "SELECT * FROM spirekeeper.dca_deposits;" \
> ${BACKUP_DIR}/dca_deposits.csv > ${BACKUP_DIR}/dca_deposits.csv
sqlite3 -header -csv /path/to/database.sqlite \ sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \ "SELECT * FROM spirekeeper.dca_clients;" \
> ${BACKUP_DIR}/dca_clients.csv > ${BACKUP_DIR}/dca_clients.csv
echo "Backup complete in ${BACKUP_DIR}/" echo "Backup complete in ${BACKUP_DIR}/"

View file

@ -38,7 +38,7 @@
-- Find duplicate transactions -- Find duplicate transactions
SELECT transaction_id, COUNT(*) as count, SELECT transaction_id, COUNT(*) as count,
STRING_AGG(id::text, ', ') as record_ids STRING_AGG(id::text, ', ') as record_ids
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
GROUP BY transaction_id GROUP BY transaction_id
HAVING COUNT(*) > 1; HAVING COUNT(*) > 1;
``` ```
@ -60,16 +60,16 @@ HAVING COUNT(*) > 1;
-- Step 1: Identify duplicate distributions -- Step 1: Identify duplicate distributions
SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount, SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount,
COUNT(dp.id) as distribution_count COUNT(dp.id) as distribution_count
FROM satoshimachine.lamassu_transactions lt FROM spirekeeper.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
GROUP BY lt.id GROUP BY lt.id
HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM satoshimachine.dca_clients WHERE remaining_balance > 0); HAVING COUNT(dp.id) > (SELECT COUNT(*) FROM spirekeeper.dca_clients WHERE remaining_balance > 0);
-- Step 2: Calculate over-distributed amounts per client -- Step 2: Calculate over-distributed amounts per client
SELECT client_id, SELECT client_id,
SUM(amount_sats) as total_received, SUM(amount_sats) as total_received,
-- Manual calculation of expected amount needed here -- Manual calculation of expected amount needed here
FROM satoshimachine.dca_payments FROM spirekeeper.dca_payments
WHERE lamassu_transaction_id IN (SELECT id FROM duplicates_table) WHERE lamassu_transaction_id IN (SELECT id FROM duplicates_table)
GROUP BY client_id; GROUP BY client_id;
``` ```
@ -94,7 +94,7 @@ if existing:
**Required Database Change**: **Required Database Change**:
```sql ```sql
ALTER TABLE satoshimachine.lamassu_transactions ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
``` ```
@ -117,7 +117,7 @@ ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Check last successful poll -- Check last successful poll
SELECT MAX(created_at) as last_transaction, SELECT MAX(created_at) as last_transaction,
EXTRACT(EPOCH FROM (NOW() - MAX(created_at)))/3600 as hours_since_last EXTRACT(EPOCH FROM (NOW() - MAX(created_at)))/3600 as hours_since_last
FROM satoshimachine.lamassu_transactions; FROM spirekeeper.lamassu_transactions;
-- If hours_since_last > 24, investigate immediately -- If hours_since_last > 24, investigate immediately
``` ```
@ -194,8 +194,8 @@ ssh-keygen -t ed25519 -f ~/.ssh/satmachine_lamassu -C "satmachine-polling"
-- Find stuck/failed payments (older than 1 hour, not completed) -- Find stuck/failed payments (older than 1 hour, not completed)
SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at, SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at,
c.username, dp.payment_hash c.username, dp.payment_hash
FROM satoshimachine.dca_payments dp FROM spirekeeper.dca_payments dp
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE dp.status != 'completed' WHERE dp.status != 'completed'
AND dp.created_at < NOW() - INTERVAL '1 hour' AND dp.created_at < NOW() - INTERVAL '1 hour'
ORDER BY dp.created_at DESC; ORDER BY dp.created_at DESC;
@ -207,7 +207,7 @@ ORDER BY dp.created_at DESC;
SELECT SELECT
SUM(commission_amount) as total_commission_expected, SUM(commission_amount) as total_commission_expected,
-- Manually check actual wallet balance in LNBits -- Manually check actual wallet balance in LNBits
FROM satoshimachine.lamassu_transactions; FROM spirekeeper.lamassu_transactions;
``` ```
#### Immediate Response #### Immediate Response
@ -258,9 +258,9 @@ SELECT
c.remaining_balance, c.remaining_balance,
COALESCE(SUM(d.amount), 0) as total_deposits, COALESCE(SUM(d.amount), 0) as total_deposits,
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments
FROM satoshimachine.dca_clients c FROM spirekeeper.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id; GROUP BY c.id;
``` ```
@ -319,9 +319,9 @@ SELECT
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_distributed, COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_distributed,
COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as calculated_balance, COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as calculated_balance,
c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0)) as discrepancy c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0)) as discrepancy
FROM satoshimachine.dca_clients c FROM spirekeeper.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id, c.username, c.remaining_balance GROUP BY c.id, c.username, c.remaining_balance
HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0))) > 1; HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0))) > 1;
``` ```
@ -341,11 +341,11 @@ HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE
```sql ```sql
-- Get complete transaction history for client -- Get complete transaction history for client
SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at
FROM satoshimachine.dca_deposits FROM spirekeeper.dca_deposits
WHERE client_id = <client_id> WHERE client_id = <client_id>
UNION ALL UNION ALL
SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL
FROM satoshimachine.dca_payments FROM spirekeeper.dca_payments
WHERE client_id = <client_id> WHERE client_id = <client_id>
ORDER BY created_at; ORDER BY created_at;
``` ```
@ -361,18 +361,18 @@ ORDER BY created_at;
**Option A: Adjustment Entry** (Recommended) **Option A: Adjustment Entry** (Recommended)
```sql ```sql
-- Create compensating deposit for positive discrepancy -- Create compensating deposit for positive discrepancy
INSERT INTO satoshimachine.dca_deposits (client_id, amount, status, note) INSERT INTO spirekeeper.dca_deposits (client_id, amount, status, note)
VALUES (<client_id>, <adjustment_amount>, 'confirmed', 'Balance correction - reconciliation 2025-10-19'); VALUES (<client_id>, <adjustment_amount>, 'confirmed', 'Balance correction - reconciliation 2025-10-19');
-- OR Create compensating payment for negative discrepancy -- OR Create compensating payment for negative discrepancy
INSERT INTO satoshimachine.dca_payments (client_id, amount_sats, status, note) INSERT INTO spirekeeper.dca_payments (client_id, amount_sats, status, note)
VALUES (<client_id>, <adjustment_amount>, 'completed', 'Balance correction - reconciliation 2025-10-19'); VALUES (<client_id>, <adjustment_amount>, 'completed', 'Balance correction - reconciliation 2025-10-19');
``` ```
**Option B: Direct Balance Update** (Use with extreme caution) **Option B: Direct Balance Update** (Use with extreme caution)
```sql ```sql
-- ONLY if audit trail is complete and discrepancy is unexplained -- ONLY if audit trail is complete and discrepancy is unexplained
UPDATE satoshimachine.dca_clients UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_balance>, SET remaining_balance = <correct_balance>,
updated_at = NOW() updated_at = NOW()
WHERE id = <client_id>; WHERE id = <client_id>;
@ -396,11 +396,11 @@ async def daily_reconciliation_check():
**Database Constraints**: **Database Constraints**:
```sql ```sql
-- Prevent negative balances -- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0); ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Prevent confirmed deposits with zero amount -- Prevent confirmed deposits with zero amount
ALTER TABLE satoshimachine.dca_deposits ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0); ADD CONSTRAINT positive_deposit CHECK (amount > 0);
``` ```
@ -435,7 +435,7 @@ SELECT
-- Calculate differences -- Calculate differences
base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as base_difference, base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as base_difference,
commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) as commission_difference commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) as commission_difference
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
WHERE ABS(base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) > 1 WHERE ABS(base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100)))) > 1
OR ABS(commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))))) > 1; OR ABS(commission_amount - ROUND(crypto_atoms - (crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))))) > 1;
``` ```
@ -485,9 +485,9 @@ SELECT
SUM(lt.base_amount) as total_distributed, SUM(lt.base_amount) as total_distributed,
SUM(expected_base) as should_have_distributed, SUM(expected_base) as should_have_distributed,
SUM(expected_base - lt.base_amount) as client_impact SUM(expected_base - lt.base_amount) as client_impact
FROM satoshimachine.lamassu_transactions lt FROM spirekeeper.lamassu_transactions lt
JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE -- filter for affected transactions WHERE -- filter for affected transactions
GROUP BY c.id; GROUP BY c.id;
``` ```
@ -505,7 +505,7 @@ GROUP BY c.id;
- Create compensating payments to affected clients: - Create compensating payments to affected clients:
```sql ```sql
-- Add to client balances -- Add to client balances
UPDATE satoshimachine.dca_clients c UPDATE spirekeeper.dca_clients c
SET remaining_balance = remaining_balance + adjustment.amount SET remaining_balance = remaining_balance + adjustment.amount
FROM ( FROM (
-- Calculate adjustment per client -- Calculate adjustment per client
@ -572,7 +572,7 @@ assert abs(calculated_total - crypto_atoms) <= 1, "Commission calculation error
SELECT SELECT
SUM(commission_amount) as expected_total_commission, SUM(commission_amount) as expected_total_commission,
-- Compare to actual wallet balance in LNBits dashboard -- Compare to actual wallet balance in LNBits dashboard
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
WHERE created_at > '<date-of-last-known-good-balance>'; WHERE created_at > '<date-of-last-known-good-balance>';
``` ```
@ -607,7 +607,7 @@ curl -X GET https://<lnbits-host>/api/v1/wallet \
SELECT commission_wallet_id, SELECT commission_wallet_id,
LEFT(commission_wallet_adminkey, 10) || '...' as key_preview, LEFT(commission_wallet_adminkey, 10) || '...' as key_preview,
updated_at updated_at
FROM satoshimachine.lamassu_config FROM spirekeeper.lamassu_config
ORDER BY updated_at DESC ORDER BY updated_at DESC
LIMIT 1; LIMIT 1;
``` ```
@ -783,9 +783,9 @@ WITH client_financials AS (
COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments, COALESCE(SUM(CASE WHEN p.status = 'completed' THEN p.amount_sats ELSE 0 END), 0) as total_payments,
COUNT(DISTINCT d.id) as deposit_count, COUNT(DISTINCT d.id) as deposit_count,
COUNT(DISTINCT p.id) as payment_count COUNT(DISTINCT p.id) as payment_count
FROM satoshimachine.dca_clients c FROM spirekeeper.dca_clients c
LEFT JOIN satoshimachine.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed' LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN satoshimachine.dca_payments p ON c.id = p.client_id LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id GROUP BY c.id
) )
SELECT SELECT
@ -813,7 +813,7 @@ SELECT
SUM(commission_amount) as total_commission, SUM(commission_amount) as total_commission,
MIN(created_at) as first_transaction, MIN(created_at) as first_transaction,
MAX(created_at) as last_transaction MAX(created_at) as last_transaction
FROM satoshimachine.lamassu_transactions; FROM spirekeeper.lamassu_transactions;
-- 3. Failed/Pending Payments Check -- 3. Failed/Pending Payments Check
SELECT SELECT
@ -822,7 +822,7 @@ SELECT
SUM(amount_sats) as total_amount, SUM(amount_sats) as total_amount,
MIN(created_at) as oldest, MIN(created_at) as oldest,
MAX(created_at) as newest MAX(created_at) as newest
FROM satoshimachine.dca_payments FROM spirekeeper.dca_payments
GROUP BY status GROUP BY status
ORDER BY ORDER BY
CASE status CASE status
@ -839,7 +839,7 @@ SELECT
status, status,
created_at, created_at,
EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending
FROM satoshimachine.dca_deposits FROM spirekeeper.dca_deposits
WHERE status = 'pending' WHERE status = 'pending'
AND created_at < NOW() - INTERVAL '48 hours' AND created_at < NOW() - INTERVAL '48 hours'
ORDER BY created_at; ORDER BY created_at;
@ -855,7 +855,7 @@ SELECT
commission_amount, commission_amount,
ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as expected_base, ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as expected_base,
base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as difference base_amount - ROUND(crypto_atoms / (1 + (commission_percentage * (100 - discount) / 100))) as difference
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 20; LIMIT 20;
``` ```
@ -876,15 +876,15 @@ psql -h localhost -U lnbits -d lnbits
2. **Direct Configuration Update**: 2. **Direct Configuration Update**:
```sql ```sql
-- Update Lamassu config directly -- Update Lamassu config directly
UPDATE satoshimachine.lamassu_config UPDATE spirekeeper.lamassu_config
SET polling_enabled = false SET polling_enabled = false
WHERE id = (SELECT MAX(id) FROM satoshimachine.lamassu_config); WHERE id = (SELECT MAX(id) FROM spirekeeper.lamassu_config);
``` ```
3. **Manual Client Balance Update**: 3. **Manual Client Balance Update**:
```sql ```sql
-- ONLY in emergency when dashboard unavailable -- ONLY in emergency when dashboard unavailable
UPDATE satoshimachine.dca_clients UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_amount> SET remaining_balance = <correct_amount>
WHERE id = <client_id>; WHERE id = <client_id>;
-- MUST document this action in incident log -- MUST document this action in incident log
@ -910,19 +910,19 @@ uv run lnbits
```bash ```bash
# Export all DCA-related tables to CSV # Export all DCA-related tables to CSV
sqlite3 -header -csv /path/to/lnbits/database.sqlite \ sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \ "SELECT * FROM spirekeeper.lamassu_transactions;" \
> lamassu_transactions_export_$(date +%Y%m%d_%H%M%S).csv > lamassu_transactions_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \ sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \ "SELECT * FROM spirekeeper.dca_payments;" \
> dca_payments_export_$(date +%Y%m%d_%H%M%S).csv > dca_payments_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \ sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \ "SELECT * FROM spirekeeper.dca_deposits;" \
> dca_deposits_export_$(date +%Y%m%d_%H%M%S).csv > dca_deposits_export_$(date +%Y%m%d_%H%M%S).csv
sqlite3 -header -csv /path/to/lnbits/database.sqlite \ sqlite3 -header -csv /path/to/lnbits/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \ "SELECT * FROM spirekeeper.dca_clients;" \
> dca_clients_export_$(date +%Y%m%d_%H%M%S).csv > dca_clients_export_$(date +%Y%m%d_%H%M%S).csv
``` ```
@ -946,9 +946,9 @@ SELECT
dp.amount_sats as client_received, dp.amount_sats as client_received,
dp.status as payment_status, dp.status as payment_status,
dp.payment_hash dp.payment_hash
FROM satoshimachine.lamassu_transactions lt FROM spirekeeper.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN satoshimachine.dca_clients c ON dp.client_id = c.id LEFT JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
ORDER BY lt.created_at DESC, c.username; ORDER BY lt.created_at DESC, c.username;
``` ```
@ -981,18 +981,18 @@ async def process_lamassu_transaction(txn_data: dict) -> Optional[LamassuTransac
```sql ```sql
-- Add unique constraint on transaction_id -- Add unique constraint on transaction_id
ALTER TABLE satoshimachine.lamassu_transactions ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id); ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Prevent negative balances -- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0); ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Ensure positive amounts -- Ensure positive amounts
ALTER TABLE satoshimachine.dca_deposits ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0); ADD CONSTRAINT positive_deposit CHECK (amount > 0);
ALTER TABLE satoshimachine.dca_payments ALTER TABLE spirekeeper.dca_payments
ADD CONSTRAINT positive_payment CHECK (amount_sats > 0); ADD CONSTRAINT positive_payment CHECK (amount_sats > 0);
``` ```
@ -1196,7 +1196,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
### System Access ### System Access
**LNBits Admin Dashboard**: **LNBits Admin Dashboard**:
- URL: `https://<your-lnbits-host>/satoshimachine` - URL: `https://<your-lnbits-host>/spirekeeper`
- Requires superuser authentication - Requires superuser authentication
**Database Access**: **Database Access**:
@ -1205,7 +1205,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
sqlite3 /home/padreug/AioLabs/Git/lnbits-extensions/lnbits/data/database.sqlite sqlite3 /home/padreug/AioLabs/Git/lnbits-extensions/lnbits/data/database.sqlite
# Direct table access # Direct table access
sqlite3 /path/to/db "SELECT * FROM satoshimachine.<table_name>;" sqlite3 /path/to/db "SELECT * FROM spirekeeper.<table_name>;"
``` ```
**Log Files**: **Log Files**:
@ -1274,7 +1274,7 @@ pkill -f lnbits
```sql ```sql
-- Disable automatic polling -- Disable automatic polling
UPDATE satoshimachine.lamassu_config UPDATE spirekeeper.lamassu_config
SET polling_enabled = false; SET polling_enabled = false;
``` ```
@ -1283,7 +1283,7 @@ SET polling_enabled = false;
```sql ```sql
-- All client balances summary -- All client balances summary
SELECT id, username, remaining_balance, created_at SELECT id, username, remaining_balance, created_at
FROM satoshimachine.dca_clients FROM spirekeeper.dca_clients
ORDER BY remaining_balance DESC; ORDER BY remaining_balance DESC;
``` ```
@ -1291,7 +1291,7 @@ ORDER BY remaining_balance DESC;
```sql ```sql
SELECT id, transaction_id, created_at, crypto_atoms, base_amount, commission_amount SELECT id, transaction_id, created_at, crypto_atoms, base_amount, commission_amount
FROM satoshimachine.lamassu_transactions FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 10; LIMIT 10;
``` ```
@ -1299,7 +1299,7 @@ LIMIT 10;
### Failed Payments ### Failed Payments
```sql ```sql
SELECT * FROM satoshimachine.dca_payments SELECT * FROM spirekeeper.dca_payments
WHERE status != 'completed' WHERE status != 'completed'
ORDER BY created_at DESC; ORDER BY created_at DESC;
``` ```
@ -1314,19 +1314,19 @@ BACKUP_DIR="emergency_backup_${DATE}"
mkdir -p $BACKUP_DIR mkdir -p $BACKUP_DIR
sqlite3 -header -csv /path/to/database.sqlite \ sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \ "SELECT * FROM spirekeeper.lamassu_transactions;" \
> ${BACKUP_DIR}/lamassu_transactions.csv > ${BACKUP_DIR}/lamassu_transactions.csv
sqlite3 -header -csv /path/to/database.sqlite \ sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \ "SELECT * FROM spirekeeper.dca_payments;" \
> ${BACKUP_DIR}/dca_payments.csv > ${BACKUP_DIR}/dca_payments.csv
sqlite3 -header -csv /path/to/database.sqlite \ sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \ "SELECT * FROM spirekeeper.dca_deposits;" \
> ${BACKUP_DIR}/dca_deposits.csv > ${BACKUP_DIR}/dca_deposits.csv
sqlite3 -header -csv /path/to/database.sqlite \ sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \ "SELECT * FROM spirekeeper.dca_clients;" \
> ${BACKUP_DIR}/dca_clients.csv > ${BACKUP_DIR}/dca_clients.csv
echo "Backup complete in ${BACKUP_DIR}/" echo "Backup complete in ${BACKUP_DIR}/"