Compare commits

..

2 commits

Author SHA1 Message Date
4ac640a499 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>
2026-06-13 22:31:21 +02:00
a059e3f596 refactor: rename extension identity to spirekeeper
Fork of satmachineadmin's v2-bitspire line into its own repo. Renames
both identifiers so this extension is fully independent of the original
satmachineadmin install (which remains in service):

  - extension id   satmachineadmin -> spirekeeper
    (router prefix, static path/static_url_for, module symbols, task
     names, templates dir, config/manifest paths)
  - database name  satoshimachine  -> spirekeeper
    (Database(ext_spirekeeper), all schema-qualified table refs)

Also resets versioning to 0.1.0, sets the display name + manifest to
spirekeeper/aiolabs, and fixes the placeholder pyproject description.
Historical aiolabs/satmachineadmin#N issue references in comments are
left pointing at the original repo where those issues live.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 22:30:05 +02:00
28 changed files with 384 additions and 385 deletions

View file

@ -213,7 +213,7 @@ commission_amount = 266800 - 258835 = 7,965 sats (to commission wallet)
### Security Considerations
- **Superuser Authentication**: Admin extension requires LNBits superuser login
- **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
- Read-only database permissions for Lamassu access
- Input sanitization and type validation
@ -230,7 +230,7 @@ on existing installs:
```sql
SELECT a.id, a.username, a.pubkey, m.id, m.machine_npub
FROM accounts a
JOIN ext_satoshimachine.dca_machines m
JOIN ext_spirekeeper.dca_machines m
ON LOWER(a.pubkey) = LOWER(m.machine_npub);
```

View file

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

View file

@ -7,29 +7,29 @@ from loguru import logger
from .crud import db
from .nostr_transport_roster import register_with_lnbits as register_roster_with_lnbits
from .tasks import wait_for_cassette_state_events, wait_for_paid_invoices
from .views import satmachineadmin_generic_router
from .views_api import satmachineadmin_api_router
from .views import spirekeeper_generic_router
from .views_api import spirekeeper_api_router
logger.info("satmachineadmin v2 loaded")
logger.info("spirekeeper v2 loaded")
satmachineadmin_ext: APIRouter = APIRouter(
prefix="/satmachineadmin", tags=["DCA Admin"]
spirekeeper_ext: APIRouter = APIRouter(
prefix="/spirekeeper", tags=["DCA Admin"]
)
satmachineadmin_ext.include_router(satmachineadmin_generic_router)
satmachineadmin_ext.include_router(satmachineadmin_api_router)
spirekeeper_ext.include_router(spirekeeper_generic_router)
spirekeeper_ext.include_router(spirekeeper_api_router)
satmachineadmin_static_files = [
spirekeeper_static_files = [
{
"path": "/satmachineadmin/static",
"name": "satmachineadmin_static",
"path": "/spirekeeper/static",
"name": "spirekeeper_static",
}
]
scheduled_tasks: list[asyncio.Task] = []
def satmachineadmin_stop():
def spirekeeper_stop():
for task in scheduled_tasks:
try:
task.cancel()
@ -37,10 +37,10 @@ def satmachineadmin_stop():
logger.warning(ex)
def satmachineadmin_start():
def spirekeeper_start():
# bitSpire invoice listener — replaces the v1 SSH/PostgreSQL poller.
invoice_task = create_permanent_unique_task(
"ext_satmachineadmin", wait_for_paid_invoices
"ext_spirekeeper", wait_for_paid_invoices
)
scheduled_tasks.append(invoice_task)
# Cassette bootstrap consumer (#29 v1) — subscribes to
@ -48,7 +48,7 @@ def satmachineadmin_start():
# cassette_configs on receipt. Soft-fails if nostrclient isn't
# installed (logs + backs off, never crashes).
cassette_task = create_permanent_unique_task(
"ext_satmachineadmin_cassette_bootstrap", wait_for_cassette_state_events
"ext_spirekeeper_cassette_bootstrap", wait_for_cassette_state_events
)
scheduled_tasks.append(cassette_task)
# Path-B wallet-routing hook (#20 / coord-log 2026-05-31T15:25Z):
@ -61,8 +61,8 @@ def satmachineadmin_start():
__all__ = [
"db",
"satmachineadmin_ext",
"satmachineadmin_start",
"satmachineadmin_static_files",
"satmachineadmin_stop",
"spirekeeper_ext",
"spirekeeper_start",
"spirekeeper_static_files",
"spirekeeper_stop",
]

View file

@ -94,7 +94,7 @@ class SettlementMetadataError(ValueError):
Raised by `parse_settlement`. Caller records the settlement as
'rejected' with the exception message in `error_message`. Operator
investigates the ATM that issued the invoice a bitSpire ATM that
landed on a satmachineadmin-managed wallet without stamping the
landed on a spirekeeper-managed wallet without stamping the
canonical fields is a real upstream bug (lamassu-next side), not a
graceful-degradation case. Pre-v2 reverse-derivation from the
wire amount + a machine-level fallback rate is no longer supported:
@ -279,7 +279,7 @@ def parse_settlement(
)
# Phase-1 observability per aiolabs/satmachineadmin#38 + coord-log
# §2026-06-01T07:00Z (option A locked): compare bitspire's reported
# fee_sats against satmachineadmin's recompute, log on out-of-
# fee_sats against spirekeeper's recompute, log on out-of-
# tolerance drift, record the delta unconditionally for triage.
# Phase 2 (settlement-reject) lands after observability data.
fee_mismatch_sats = fee_sats - (platform_fee_sats + operator_fee_sats)

View file

@ -33,7 +33,7 @@ task (tasks.py) calls `decrypt_and_parse_state_event` per incoming event;
the API endpoint (views_api.py) calls `publish_to_atm` per operator submit.
The `<m>` placeholder semantics (load-bearing per the 2026-05-30T11:50Z
coord-log entry): always the ATM's hex pubkey, NEVER satmachineadmin's
coord-log entry): always the ATM's hex pubkey, NEVER spirekeeper's
internal dca_machines.id UUID. Helper `_atm_hex_pubkey(machine)`
centralises the canonicalisation via lnbits.utils.nostr.normalize_public_key.
"""
@ -146,7 +146,7 @@ def build_state_d_tags_for_machines(machines: list[Machine]) -> list[str]:
# =============================================================================
# Publish — operator → ATM (the satmachineadmin API path)
# Publish — operator → ATM (the spirekeeper API path)
# =============================================================================

View file

@ -1,7 +1,7 @@
{
"name": "DCA Admin",
"name": "spirekeeper",
"short_description": "Dollar Cost Averaging administration for Lamassu ATM integration",
"tile": "/satmachineadmin/static/image/aio.png",
"tile": "/spirekeeper/static/image/aio.png",
"min_lnbits_version": "1.0.0",
"contributors": [
{
@ -26,7 +26,7 @@
}
],
"images": [],
"description_md": "/satmachineadmin/description.md",
"terms_and_conditions_md": "/satmachineadmin/toc.md",
"description_md": "/spirekeeper/description.md",
"terms_and_conditions_md": "/spirekeeper/toc.md",
"license": "MIT"
}

156
crud.py
View file

@ -38,7 +38,7 @@ from .models import (
UpsertDcaLpData,
)
db = Database("ext_satoshimachine")
db = Database("ext_spirekeeper")
# =============================================================================
@ -48,7 +48,7 @@ db = Database("ext_satoshimachine")
async def get_super_config() -> SuperConfig | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.super_config WHERE id = :id",
"SELECT * FROM spirekeeper.super_config WHERE id = :id",
{"id": "default"},
SuperConfig,
)
@ -62,7 +62,7 @@ async def update_super_config(data: UpdateSuperConfigData) -> SuperConfig | None
set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["id"] = "default"
await db.execute(
f"UPDATE satoshimachine.super_config SET {set_clause} WHERE id = :id",
f"UPDATE spirekeeper.super_config SET {set_clause} WHERE id = :id",
update_data,
)
return await get_super_config()
@ -78,7 +78,7 @@ async def create_machine(operator_user_id: str, data: CreateMachineData) -> Mach
now = datetime.now()
await db.execute(
"""
INSERT INTO satoshimachine.dca_machines
INSERT INTO spirekeeper.dca_machines
(id, operator_user_id, machine_npub, wallet_id, name, location,
fiat_code, is_active,
operator_cash_in_fee_fraction, operator_cash_out_fee_fraction,
@ -110,7 +110,7 @@ async def create_machine(operator_user_id: str, data: CreateMachineData) -> Mach
async def get_machine(machine_id: str) -> Machine | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_machines WHERE id = :id",
"SELECT * FROM spirekeeper.dca_machines WHERE id = :id",
{"id": machine_id},
Machine,
)
@ -118,7 +118,7 @@ async def get_machine(machine_id: str) -> Machine | None:
async def get_machine_by_npub(machine_npub: str) -> Machine | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_machines WHERE machine_npub = :npub",
"SELECT * FROM spirekeeper.dca_machines WHERE machine_npub = :npub",
{"npub": machine_npub},
Machine,
)
@ -128,7 +128,7 @@ async def get_active_machine_by_wallet_id(wallet_id: str) -> Machine | None:
"""Used by the invoice listener to route an incoming payment to a machine."""
return await db.fetchone(
"""
SELECT * FROM satoshimachine.dca_machines
SELECT * FROM spirekeeper.dca_machines
WHERE wallet_id = :wid AND is_active = true
LIMIT 1
""",
@ -140,7 +140,7 @@ async def get_active_machine_by_wallet_id(wallet_id: str) -> Machine | None:
async def get_machines_for_operator(operator_user_id: str) -> list[Machine]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_machines
SELECT * FROM spirekeeper.dca_machines
WHERE operator_user_id = :uid
ORDER BY created_at DESC
""",
@ -157,7 +157,7 @@ async def list_all_active_machines() -> list[Machine]:
"""
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_machines
SELECT * FROM spirekeeper.dca_machines
WHERE is_active = true
ORDER BY created_at DESC
""",
@ -196,7 +196,7 @@ async def update_machine(machine_id: str, data: UpdateMachineData) -> Machine |
set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["id"] = machine_id
await db.execute(
f"UPDATE satoshimachine.dca_machines SET {set_clause} WHERE id = :id",
f"UPDATE spirekeeper.dca_machines SET {set_clause} WHERE id = :id",
update_data,
)
return await get_machine(machine_id)
@ -204,7 +204,7 @@ async def update_machine(machine_id: str, data: UpdateMachineData) -> Machine |
async def delete_machine(machine_id: str) -> None:
await db.execute(
"DELETE FROM satoshimachine.dca_machines WHERE id = :id",
"DELETE FROM spirekeeper.dca_machines WHERE id = :id",
{"id": machine_id},
)
@ -226,7 +226,7 @@ async def create_dca_client(data: CreateDcaClientData) -> DcaClient:
now = datetime.now()
await db.execute(
"""
INSERT INTO satoshimachine.dca_clients
INSERT INTO spirekeeper.dca_clients
(id, machine_id, user_id, username, status, created_at, updated_at)
VALUES (:id, :machine_id, :user_id, :username, :status,
:created_at, :updated_at)
@ -255,8 +255,8 @@ _CLIENT_SELECT = """
(lp.user_id IS NOT NULL) AS lp_onboarded
"""
_CLIENT_FROM = (
"satoshimachine.dca_clients c "
"LEFT JOIN satoshimachine.dca_lp lp ON lp.user_id = c.user_id"
"spirekeeper.dca_clients c "
"LEFT JOIN spirekeeper.dca_lp lp ON lp.user_id = c.user_id"
)
@ -299,7 +299,7 @@ async def get_dca_clients_for_operator(operator_user_id: str) -> list[DcaClient]
f"""
SELECT {_CLIENT_SELECT}
FROM {_CLIENT_FROM}
JOIN satoshimachine.dca_machines m ON m.id = c.machine_id
JOIN spirekeeper.dca_machines m ON m.id = c.machine_id
WHERE m.operator_user_id = :uid
ORDER BY c.created_at DESC
""",
@ -332,8 +332,8 @@ async def get_flow_mode_clients_for_machine(machine_id: str) -> list[DcaClient]:
return await db.fetchall(
"""
SELECT c.*
FROM satoshimachine.dca_clients c
JOIN satoshimachine.dca_lp lp ON lp.user_id = c.user_id
FROM spirekeeper.dca_clients c
JOIN spirekeeper.dca_lp lp ON lp.user_id = c.user_id
WHERE c.machine_id = :machine_id
AND lp.default_dca_mode = 'flow'
AND c.status = 'active'
@ -353,7 +353,7 @@ async def get_dca_lp(user_id: str) -> DcaLpPreferences | None:
"""Return the LP's preferences row, or None if they haven't onboarded
via satmachineclient yet."""
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_lp WHERE user_id = :uid",
"SELECT * FROM spirekeeper.dca_lp WHERE user_id = :uid",
{"uid": user_id},
DcaLpPreferences,
)
@ -362,7 +362,7 @@ async def get_dca_lp(user_id: str) -> DcaLpPreferences | None:
async def lp_is_onboarded(user_id: str) -> bool:
"""Cheap existence check used by the deposit-creation gate."""
row = await db.fetchone(
"SELECT user_id FROM satoshimachine.dca_lp WHERE user_id = :uid",
"SELECT user_id FROM spirekeeper.dca_lp WHERE user_id = :uid",
{"uid": user_id},
)
return row is not None
@ -392,7 +392,7 @@ async def upsert_dca_lp(
)
await db.execute(
"""
INSERT INTO satoshimachine.dca_lp
INSERT INTO spirekeeper.dca_lp
(user_id, dca_wallet_id, default_dca_mode, fixed_mode_daily_limit,
autoforward_ln_address, autoforward_enabled,
created_at, updated_at)
@ -417,7 +417,7 @@ async def upsert_dca_lp(
set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["uid"] = user_id
await db.execute(
f"UPDATE satoshimachine.dca_lp SET {set_clause} WHERE user_id = :uid",
f"UPDATE spirekeeper.dca_lp SET {set_clause} WHERE user_id = :uid",
update_data,
)
refreshed = await get_dca_lp(user_id)
@ -435,7 +435,7 @@ async def update_dca_client(
set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["id"] = client_id
await db.execute(
f"UPDATE satoshimachine.dca_clients SET {set_clause} WHERE id = :id",
f"UPDATE spirekeeper.dca_clients SET {set_clause} WHERE id = :id",
update_data,
)
return await get_dca_client(client_id)
@ -443,7 +443,7 @@ async def update_dca_client(
async def delete_dca_client(client_id: str) -> None:
await db.execute(
"DELETE FROM satoshimachine.dca_clients WHERE id = :id",
"DELETE FROM spirekeeper.dca_clients WHERE id = :id",
{"id": client_id},
)
@ -466,7 +466,7 @@ async def create_deposit(
deposit_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satoshimachine.dca_deposits
INSERT INTO spirekeeper.dca_deposits
(id, client_id, machine_id, creator_user_id, amount, currency,
status, notes, created_at)
VALUES (:id, :client_id, :machine_id, :creator_user_id, :amount,
@ -491,7 +491,7 @@ async def create_deposit(
async def get_deposit(deposit_id: str) -> DcaDeposit | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_deposits WHERE id = :id",
"SELECT * FROM spirekeeper.dca_deposits WHERE id = :id",
{"id": deposit_id},
DcaDeposit,
)
@ -500,7 +500,7 @@ async def get_deposit(deposit_id: str) -> DcaDeposit | None:
async def get_deposits_for_client(client_id: str) -> list[DcaDeposit]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_deposits
SELECT * FROM spirekeeper.dca_deposits
WHERE client_id = :client_id
ORDER BY created_at DESC
""",
@ -513,8 +513,8 @@ async def get_deposits_for_operator(operator_user_id: str) -> list[DcaDeposit]:
return await db.fetchall(
"""
SELECT d.*
FROM satoshimachine.dca_deposits d
JOIN satoshimachine.dca_machines m ON m.id = d.machine_id
FROM spirekeeper.dca_deposits d
JOIN spirekeeper.dca_machines m ON m.id = d.machine_id
WHERE m.operator_user_id = :uid
ORDER BY d.created_at DESC
""",
@ -532,7 +532,7 @@ async def update_deposit(
set_clause = ", ".join(f"{k} = :{k}" for k in update_data)
update_data["id"] = deposit_id
await db.execute(
f"UPDATE satoshimachine.dca_deposits SET {set_clause} WHERE id = :id",
f"UPDATE spirekeeper.dca_deposits SET {set_clause} WHERE id = :id",
update_data,
)
return await get_deposit(deposit_id)
@ -549,7 +549,7 @@ async def update_deposit_status(
}
await db.execute(
"""
UPDATE satoshimachine.dca_deposits
UPDATE spirekeeper.dca_deposits
SET status = :status,
notes = COALESCE(:notes, notes),
confirmed_at = COALESCE(:confirmed_at, confirmed_at)
@ -562,7 +562,7 @@ async def update_deposit_status(
async def delete_deposit(deposit_id: str) -> None:
await db.execute(
"DELETE FROM satoshimachine.dca_deposits WHERE id = :id",
"DELETE FROM spirekeeper.dca_deposits WHERE id = :id",
{"id": deposit_id},
)
@ -598,7 +598,7 @@ async def create_settlement_idempotent(
settlement_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satoshimachine.dca_settlements
INSERT INTO spirekeeper.dca_settlements
(id, machine_id, payment_hash, bitspire_event_id, bitspire_txid,
wire_sats, fiat_amount, fiat_code, exchange_rate, principal_sats,
fee_sats, platform_fee_sats, operator_fee_sats, fee_mismatch_sats,
@ -639,7 +639,7 @@ async def create_settlement_idempotent(
async def get_settlement(settlement_id: str) -> DcaSettlement | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_settlements WHERE id = :id",
"SELECT * FROM spirekeeper.dca_settlements WHERE id = :id",
{"id": settlement_id},
DcaSettlement,
)
@ -650,7 +650,7 @@ async def get_settlement_by_payment_hash(
) -> DcaSettlement | None:
return await db.fetchone(
"""
SELECT * FROM satoshimachine.dca_settlements
SELECT * FROM spirekeeper.dca_settlements
WHERE payment_hash = :hash
""",
{"hash": payment_hash},
@ -663,7 +663,7 @@ async def get_settlements_for_machine(
) -> list[DcaSettlement]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_settlements
SELECT * FROM spirekeeper.dca_settlements
WHERE machine_id = :machine_id
ORDER BY created_at DESC
LIMIT :lim
@ -698,8 +698,8 @@ async def get_stuck_settlements_for_operator(
rejected = await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid AND s.status = 'rejected'
ORDER BY s.created_at DESC
""",
@ -709,8 +709,8 @@ async def get_stuck_settlements_for_operator(
errored = await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid AND s.status = 'errored'
ORDER BY s.created_at DESC
""",
@ -720,8 +720,8 @@ async def get_stuck_settlements_for_operator(
stuck_pending = await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid
AND s.status = 'pending'
AND s.created_at < :threshold
@ -733,8 +733,8 @@ async def get_stuck_settlements_for_operator(
stuck_processing = await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid
AND s.status = 'processing'
AND s.created_at < :threshold
@ -763,7 +763,7 @@ async def force_reset_stuck_settlement(
decision."""
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET status = 'errored',
processing_claim = NULL,
error_message = 'force-reset by operator (was stuck)'
@ -780,8 +780,8 @@ async def get_settlements_for_operator(
return await db.fetchall(
"""
SELECT s.*
FROM satoshimachine.dca_settlements s
JOIN satoshimachine.dca_machines m ON m.id = s.machine_id
FROM spirekeeper.dca_settlements s
JOIN spirekeeper.dca_machines m ON m.id = s.machine_id
WHERE m.operator_user_id = :uid
ORDER BY s.created_at DESC
LIMIT :lim
@ -801,7 +801,7 @@ async def mark_settlement_status(
fresh claim attempt won't see a stale token."""
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET status = :status,
error_message = :err,
processed_at = CASE
@ -840,7 +840,7 @@ async def claim_settlement_for_processing(
token = urlsafe_short_hash()
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET status = 'processing', processing_claim = :token
WHERE id = :id AND status = 'pending'
""",
@ -862,7 +862,7 @@ async def reset_settlement_for_retry(
are left in place we never re-pay sats that already moved."""
await db.execute(
"""
UPDATE satoshimachine.dca_payments
UPDATE spirekeeper.dca_payments
SET status = 'voided'
WHERE settlement_id = :sid AND status = 'failed'
""",
@ -870,7 +870,7 @@ async def reset_settlement_for_retry(
)
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET status = 'pending',
error_message = NULL,
processing_claim = NULL,
@ -902,7 +902,7 @@ async def apply_partial_dispense(
can re-distribute via the existing idempotent path."""
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET wire_sats = :gross,
principal_sats = :principal,
fee_sats = :commission,
@ -937,7 +937,7 @@ async def count_completed_legs_for_settlement(settlement_id: str) -> int:
successfully moved sats (Lightning payments can't be clawed back)."""
row = await db.fetchone(
"""
SELECT COUNT(*) AS n FROM satoshimachine.dca_payments
SELECT COUNT(*) AS n FROM spirekeeper.dca_payments
WHERE settlement_id = :sid AND status = 'completed'
""",
{"sid": settlement_id},
@ -957,7 +957,7 @@ async def append_settlement_note(
formatted = f"[{ts} by {author_user_id}] {note}"
await db.execute(
"""
UPDATE satoshimachine.dca_settlements
UPDATE spirekeeper.dca_settlements
SET notes = CASE
WHEN notes IS NULL OR notes = '' THEN :note
ELSE :note || char(10) || char(10) || notes
@ -977,7 +977,7 @@ async def void_open_legs_for_settlement(settlement_id: str) -> None:
writes its own (possibly different) skipped reasons."""
await db.execute(
"""
UPDATE satoshimachine.dca_payments
UPDATE spirekeeper.dca_payments
SET status = 'voided'
WHERE settlement_id = :sid
AND status IN ('pending', 'failed', 'skipped')
@ -1002,7 +1002,7 @@ async def get_commission_splits(
if machine_id is None:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_commission_splits
SELECT * FROM spirekeeper.dca_commission_splits
WHERE operator_user_id = :uid AND machine_id IS NULL
ORDER BY sort_order ASC
""",
@ -1011,7 +1011,7 @@ async def get_commission_splits(
)
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_commission_splits
SELECT * FROM spirekeeper.dca_commission_splits
WHERE operator_user_id = :uid AND machine_id = :mid
ORDER BY sort_order ASC
""",
@ -1040,7 +1040,7 @@ async def replace_commission_splits(
if machine_id is None:
await db.execute(
"""
DELETE FROM satoshimachine.dca_commission_splits
DELETE FROM spirekeeper.dca_commission_splits
WHERE operator_user_id = :uid AND machine_id IS NULL
""",
{"uid": operator_user_id},
@ -1048,7 +1048,7 @@ async def replace_commission_splits(
else:
await db.execute(
"""
DELETE FROM satoshimachine.dca_commission_splits
DELETE FROM spirekeeper.dca_commission_splits
WHERE operator_user_id = :uid AND machine_id = :mid
""",
{"uid": operator_user_id, "mid": machine_id},
@ -1057,7 +1057,7 @@ async def replace_commission_splits(
for leg in legs:
await db.execute(
"""
INSERT INTO satoshimachine.dca_commission_splits
INSERT INTO spirekeeper.dca_commission_splits
(id, machine_id, operator_user_id, target, label, fraction,
sort_order, created_at)
VALUES (:id, :machine_id, :uid, :target, :label, :fraction,
@ -1086,7 +1086,7 @@ async def create_dca_payment(data: CreateDcaPaymentData) -> DcaPayment:
payment_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satoshimachine.dca_payments
INSERT INTO spirekeeper.dca_payments
(id, settlement_id, client_id, machine_id, operator_user_id,
leg_type, destination_wallet_id, destination_ln_address,
amount_sats, amount_fiat, exchange_rate, transaction_time,
@ -1122,7 +1122,7 @@ async def create_dca_payment(data: CreateDcaPaymentData) -> DcaPayment:
async def get_dca_payment(payment_id: str) -> DcaPayment | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_payments WHERE id = :id",
"SELECT * FROM spirekeeper.dca_payments WHERE id = :id",
{"id": payment_id},
DcaPayment,
)
@ -1131,7 +1131,7 @@ async def get_dca_payment(payment_id: str) -> DcaPayment | None:
async def get_payments_for_settlement(settlement_id: str) -> list[DcaPayment]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE settlement_id = :sid
ORDER BY created_at ASC
""",
@ -1143,7 +1143,7 @@ async def get_payments_for_settlement(settlement_id: str) -> list[DcaPayment]:
async def get_payments_for_client(client_id: str) -> list[DcaPayment]:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE client_id = :cid
ORDER BY created_at DESC
""",
@ -1158,7 +1158,7 @@ async def get_payments_for_operator(
if leg_type is None:
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE operator_user_id = :uid
ORDER BY created_at DESC
LIMIT :lim
@ -1168,7 +1168,7 @@ async def get_payments_for_operator(
)
return await db.fetchall(
"""
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE operator_user_id = :uid AND leg_type = :leg
ORDER BY created_at DESC
LIMIT :lim
@ -1186,7 +1186,7 @@ async def update_payment_status(
) -> DcaPayment | None:
await db.execute(
"""
UPDATE satoshimachine.dca_payments
UPDATE spirekeeper.dca_payments
SET status = :status,
external_payment_hash = COALESCE(:hash, external_payment_hash),
error_message = :err
@ -1221,7 +1221,7 @@ async def get_client_balance_summary(
deposits_row = await db.fetchone(
"""
SELECT COALESCE(SUM(amount), 0) AS total
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE client_id = :cid AND status = 'confirmed'
""",
{"cid": client_id},
@ -1231,7 +1231,7 @@ async def get_client_balance_summary(
payments_row = await db.fetchone(
"""
SELECT COALESCE(SUM(amount_fiat), 0) AS total
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
WHERE client_id = :cid
AND leg_type IN ('dca', 'settlement')
AND status = 'completed'
@ -1260,7 +1260,7 @@ async def get_client_balance_summary(
async def get_telemetry(machine_id: str) -> TelemetrySnapshot | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.dca_telemetry WHERE machine_id = :mid",
"SELECT * FROM spirekeeper.dca_telemetry WHERE machine_id = :mid",
{"mid": machine_id},
TelemetrySnapshot,
)
@ -1290,7 +1290,7 @@ async def upsert_beacon_snapshot(
if existing is None:
await db.execute(
"""
INSERT INTO satoshimachine.dca_telemetry
INSERT INTO spirekeeper.dca_telemetry
(machine_id, beacon_cash_in, beacon_cash_out, beacon_cash_level,
beacon_fiat, beacon_model, beacon_name, beacon_location,
beacon_geo, beacon_fees_json, beacon_limits_json,
@ -1319,7 +1319,7 @@ async def upsert_beacon_snapshot(
else:
await db.execute(
"""
UPDATE satoshimachine.dca_telemetry SET
UPDATE spirekeeper.dca_telemetry SET
beacon_cash_in = COALESCE(:cash_in, beacon_cash_in),
beacon_cash_out = COALESCE(:cash_out, beacon_cash_out),
beacon_cash_level = COALESCE(:cash_level, beacon_cash_level),
@ -1366,7 +1366,7 @@ async def upsert_fleet_snapshot(
if existing is None:
await db.execute(
"""
INSERT INTO satoshimachine.dca_telemetry
INSERT INTO spirekeeper.dca_telemetry
(machine_id, telemetry_json, telemetry_received_at)
VALUES (:mid, :json, :now)
""",
@ -1375,7 +1375,7 @@ async def upsert_fleet_snapshot(
else:
await db.execute(
"""
UPDATE satoshimachine.dca_telemetry
UPDATE spirekeeper.dca_telemetry
SET telemetry_json = :json, telemetry_received_at = :now
WHERE machine_id = :mid
""",
@ -1416,7 +1416,7 @@ async def get_cassette_config(
machine_id: str, position: int
) -> CassetteConfig | None:
return await db.fetchone(
"SELECT * FROM satoshimachine.cassette_configs "
"SELECT * FROM spirekeeper.cassette_configs "
"WHERE machine_id = :mid AND position = :pos",
{"mid": machine_id, "pos": position},
CassetteConfig,
@ -1427,7 +1427,7 @@ async def list_cassette_configs_for_machine(
machine_id: str,
) -> list[CassetteConfig]:
return await db.fetchall(
"SELECT * FROM satoshimachine.cassette_configs "
"SELECT * FROM spirekeeper.cassette_configs "
"WHERE machine_id = :mid ORDER BY position",
{"mid": machine_id},
CassetteConfig,
@ -1459,7 +1459,7 @@ async def update_cassette_config(
update_data["mid"] = machine_id
update_data["pos"] = position
await db.execute(
f"UPDATE satoshimachine.cassette_configs SET {set_clause} "
f"UPDATE spirekeeper.cassette_configs SET {set_clause} "
"WHERE machine_id = :mid AND position = :pos",
update_data,
)
@ -1487,7 +1487,7 @@ async def apply_bootstrap_state(
events land + the operator subsequently edits.
"""
existing_first: dict | None = await db.fetchone(
"SELECT state_event_id FROM satoshimachine.cassette_configs "
"SELECT state_event_id FROM spirekeeper.cassette_configs "
"WHERE machine_id = :mid LIMIT 1",
{"mid": machine_id},
)
@ -1505,7 +1505,7 @@ async def apply_bootstrap_state(
for pos, row in payload.positions.items():
await db.execute(
"""
INSERT INTO satoshimachine.cassette_configs
INSERT INTO spirekeeper.cassette_configs
(machine_id, position, denomination, count, updated_at,
updated_by, state_denomination, state_count, state_at,
state_event_id)

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
- Use cases
...and some other text about just how great this etension is.
Requires LNbits superuser access for administration. The Lightning ATM side is served by the bitSpire device; this extension is the back office.

View file

@ -7,7 +7,7 @@
## 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.
@ -66,7 +66,7 @@ Lamassu's old answer here was TLS cert pinning. We have a richer toolbox — Nos
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
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
@ -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. |
| **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. |
| **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. |
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 |
|---|---|---|
| `~/dev/shared/extensions/satmachineadmin/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/satmachineadmin/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/satmachineadmin/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/tasks.py` | LNbits invoice listener. Entry point for all settlements today. | `_handle_payment:56-95` — loadbearing routing. |
| `~/dev/shared/extensions/spirekeeper/bitspire.py` | Parses Payment.extra. The trust boundary. | `parse_settlement:68-92` — happy vs fallback path. |
| `~/dev/shared/extensions/spirekeeper/distribution.py` | Threeleg distribution chain. | `process_settlement` — uses claim pattern. |
| `~/dev/shared/extensions/spirekeeper/crud.py` | Operatorscoped DB layer. | `claim_settlement_for_processing`, `_machine_owned_by`. |
| `~/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/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/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. |
@ -397,7 +397,7 @@ How we'd test the proposed design endtoend, once S0S5 land:
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)."
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.

View file

@ -132,7 +132,7 @@ async def publish_fee_config(
)
except (SignerUnavailable, RelayUnavailable) as exc:
logger.warning(
f"satmachineadmin: fee-config publish soft-fail for machine "
f"spirekeeper: fee-config publish soft-fail for machine "
f"{machine.id} ({machine.name or machine.machine_npub[:12]}): "
f"{type(exc).__name__}: {exc}. Underlying CRUD operation "
"succeeded; operator can re-trigger publish via the next "
@ -144,7 +144,7 @@ async def publish_fee_config(
# don't break the caller's CRUD path; a future publish attempt
# (next machine edit / next super edit) will retry.
logger.warning(
f"satmachineadmin: fee-config publish unexpected transport "
f"spirekeeper: fee-config publish unexpected transport "
f"error for machine {machine.id}: {type(exc).__name__}: {exc}"
)
return None

View file

@ -1,9 +1,9 @@
{
"repos": [
{
"id": "satmachineadmin",
"organisation": "atitlanio",
"repository": "satmachineadmin"
"id": "spirekeeper",
"organisation": "aiolabs",
"repository": "spirekeeper"
}
]
}

View file

@ -56,11 +56,11 @@ async def m001_satmachine_v2_initial(db):
"dca_deposits",
"dca_clients",
):
await db.execute(f"DROP TABLE IF EXISTS satoshimachine.{table}")
await db.execute(f"DROP TABLE IF EXISTS spirekeeper.{table}")
# 2. super_config — singleton (id='default') with platform-fee config.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.super_config (
CREATE TABLE IF NOT EXISTS spirekeeper.super_config (
id TEXT PRIMARY KEY,
super_fee_fraction DECIMAL(10,4) NOT NULL DEFAULT 0.0000,
super_fee_wallet_id TEXT,
@ -68,18 +68,18 @@ async def m001_satmachine_v2_initial(db):
);
""")
existing = await db.fetchone(
"SELECT id FROM satoshimachine.super_config WHERE id = 'default'"
"SELECT id FROM spirekeeper.super_config WHERE id = 'default'"
)
if not existing:
await db.execute(
"INSERT INTO satoshimachine.super_config (id, super_fee_fraction) "
"INSERT INTO spirekeeper.super_config (id, super_fee_fraction) "
"VALUES ('default', 0.0000)"
)
# 3. dca_machines — one row per bitSpire ATM, owned by exactly one
# operator. wallet_id UNIQUE prevents the IDOR funds-theft vector.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_machines (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_machines (
id TEXT PRIMARY KEY,
operator_user_id TEXT NOT NULL,
machine_npub TEXT NOT NULL UNIQUE,
@ -107,7 +107,7 @@ async def m001_satmachine_v2_initial(db):
# just decides "this LP is enrolled at my machine"; everything
# delivery-related is the LP's own preference.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_clients (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_clients (
id TEXT PRIMARY KEY,
machine_id TEXT NOT NULL,
user_id TEXT NOT NULL,
@ -129,17 +129,17 @@ async def m001_satmachine_v2_initial(db):
# user that has onboarded as a Liquidity Provider, regardless of
# how many machines they're enrolled at. Owned by the LP (writes
# come from the satmachineclient extension under the LP's session),
# read by satmachineadmin during distribution to resolve "where do
# read by spirekeeper during distribution to resolve "where do
# DCA payouts for this LP go?"
#
# Gating: satmachineadmin refuses to create deposits for an LP who
# Gating: spirekeeper refuses to create deposits for an LP who
# doesn't have a dca_lp row yet. The LP must onboard via
# satmachineclient first (which auto-creates the row with their
# default LNbits wallet on first dashboard visit). Forces every
# LP through a "yes, I am here and this is where I want my sats"
# gesture before any fiat starts accumulating against them.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_lp (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_lp (
user_id TEXT PRIMARY KEY,
dca_wallet_id TEXT NOT NULL,
default_dca_mode TEXT NOT NULL DEFAULT 'flow',
@ -154,7 +154,7 @@ async def m001_satmachine_v2_initial(db):
# 5. dca_deposits — fiat the operator (or super) records against an LP
# at a machine. creator_user_id preserves audit trail.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_deposits (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_deposits (
id TEXT PRIMARY KEY,
client_id TEXT NOT NULL,
machine_id TEXT NOT NULL,
@ -184,7 +184,7 @@ async def m001_satmachine_v2_initial(db):
# forgave what per transaction. Do not collapse them into a single
# fee_fraction. See plan section "Customer discounts" and #10.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_settlements (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_settlements (
id TEXT PRIMARY KEY,
machine_id TEXT NOT NULL,
payment_hash TEXT NOT NULL UNIQUE,
@ -227,7 +227,7 @@ async def m001_satmachine_v2_initial(db):
# - LNURL string (bech32 LNURL...)
# Resolution lives in distribution._pay_one_split_leg.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_commission_splits (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_commission_splits (
id TEXT PRIMARY KEY,
machine_id TEXT,
operator_user_id TEXT NOT NULL,
@ -248,7 +248,7 @@ async def m001_satmachine_v2_initial(db):
# autoforward | refund. status enum: pending | completed | failed |
# voided | skipped | refunded.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_payments (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_payments (
id TEXT PRIMARY KEY,
settlement_id TEXT,
client_id TEXT,
@ -287,7 +287,7 @@ async def m001_satmachine_v2_initial(db):
# (name, location, geo, fees, limits, denominations, version) are
# nullable until that upstream issue lands. Ingest opportunistically.
await db.execute("""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_telemetry (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_telemetry (
machine_id TEXT PRIMARY KEY,
beacon_cash_in BOOLEAN,
beacon_cash_out BOOLEAN,
@ -315,7 +315,7 @@ async def m002_rename_commission_split_wallet_id_to_target(db):
EXISTS`, which is a no-op when the table already exists so the
schema drift survives the documented uninstall + reinstall workflow
because LNbits' uninstall wipes the dbversions tracker but NOT the
satoshimachine.sqlite3 file on disk.
spirekeeper.sqlite3 file on disk.
Idempotent: probes for the `wallet_id` column via a SELECT. If the
probe succeeds the column still exists and we RENAME it; otherwise
@ -326,14 +326,14 @@ async def m002_rename_commission_split_wallet_id_to_target(db):
"""
try:
await db.fetchone(
"SELECT wallet_id FROM satoshimachine.dca_commission_splits LIMIT 1"
"SELECT wallet_id FROM spirekeeper.dca_commission_splits LIMIT 1"
)
except Exception:
# wallet_id column doesn't exist; either m001 produced the correct
# schema on a fresh install or the rename already landed.
return
await db.execute(
"ALTER TABLE satoshimachine.dca_commission_splits "
"ALTER TABLE spirekeeper.dca_commission_splits "
"RENAME COLUMN wallet_id TO target"
)
@ -350,11 +350,11 @@ async def m003_rename_settlements_net_sats_to_principal_sats(db):
Idempotent: probes for the old `net_sats` column. If present, rename.
"""
try:
await db.fetchone("SELECT net_sats FROM satoshimachine.dca_settlements LIMIT 1")
await db.fetchone("SELECT net_sats FROM spirekeeper.dca_settlements LIMIT 1")
except Exception:
return
await db.execute(
"ALTER TABLE satoshimachine.dca_settlements "
"ALTER TABLE spirekeeper.dca_settlements "
"RENAME COLUMN net_sats TO principal_sats"
)
@ -383,14 +383,14 @@ async def m004_introduce_dca_lp_table(db):
absent the install already on the new shape; no-op.
"""
try:
await db.fetchone("SELECT wallet_id FROM satoshimachine.dca_clients LIMIT 1")
await db.fetchone("SELECT wallet_id FROM spirekeeper.dca_clients LIMIT 1")
except Exception:
return
# Step 1: create dca_lp if it doesn't exist yet. m001 on a fresh install
# already created it; on a pre-m004 install we're creating it here.
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.dca_lp (
CREATE TABLE IF NOT EXISTS spirekeeper.dca_lp (
user_id TEXT PRIMARY KEY,
dca_wallet_id TEXT NOT NULL,
default_dca_mode TEXT NOT NULL DEFAULT 'flow',
@ -407,7 +407,7 @@ async def m004_introduce_dca_lp_table(db):
# enrolled at multiple machines — that row reflects their most
# recent intent. ROW_NUMBER() OVER (...) requires SQLite 3.25+ (2018).
await db.execute("""
INSERT OR IGNORE INTO satoshimachine.dca_lp
INSERT OR IGNORE INTO spirekeeper.dca_lp
(user_id, dca_wallet_id, default_dca_mode, fixed_mode_daily_limit,
autoforward_ln_address, autoforward_enabled,
created_at, updated_at)
@ -419,7 +419,7 @@ async def m004_introduce_dca_lp_table(db):
PARTITION BY user_id
ORDER BY updated_at DESC, created_at DESC
) AS rn
FROM satoshimachine.dca_clients
FROM spirekeeper.dca_clients
) ranked
WHERE rn = 1
""")
@ -434,7 +434,7 @@ async def m004_introduce_dca_lp_table(db):
"autoforward_ln_address",
"autoforward_enabled",
):
await db.execute(f"ALTER TABLE satoshimachine.dca_clients DROP COLUMN {col}")
await db.execute(f"ALTER TABLE spirekeeper.dca_clients DROP COLUMN {col}")
async def m006_rename_to_canonical_sat_vocabulary(db):
@ -474,13 +474,13 @@ async def m006_rename_to_canonical_sat_vocabulary(db):
]
for table, old_col, new_col in renames:
try:
await db.fetchone(f"SELECT {old_col} FROM satoshimachine.{table} LIMIT 1")
await db.fetchone(f"SELECT {old_col} FROM spirekeeper.{table} LIMIT 1")
except Exception:
# old column doesn't exist; either rename already landed or
# m001 produced the canonical schema directly on fresh install.
continue
await db.execute(
f"ALTER TABLE satoshimachine.{table} "
f"ALTER TABLE spirekeeper.{table} "
f"RENAME COLUMN {old_col} TO {new_col}"
)
@ -494,11 +494,11 @@ async def m006_rename_to_canonical_sat_vocabulary(db):
]
for table, col in drops:
try:
await db.fetchone(f"SELECT {col} FROM satoshimachine.{table} LIMIT 1")
await db.fetchone(f"SELECT {col} FROM spirekeeper.{table} LIMIT 1")
except Exception:
# column doesn't exist; either already dropped or never present.
continue
await db.execute(f"ALTER TABLE satoshimachine.{table} DROP COLUMN {col}")
await db.execute(f"ALTER TABLE spirekeeper.{table} DROP COLUMN {col}")
async def m005_lock_deposit_currency_to_machine_fiat_code(db):
@ -518,15 +518,15 @@ async def m005_lock_deposit_currency_to_machine_fiat_code(db):
no mismatches it's a no-op UPDATE.
"""
await db.execute("""
UPDATE satoshimachine.dca_deposits AS d
UPDATE spirekeeper.dca_deposits AS d
SET currency = (
SELECT m.fiat_code
FROM satoshimachine.dca_machines m
FROM spirekeeper.dca_machines m
WHERE m.id = d.machine_id
)
WHERE EXISTS (
SELECT 1
FROM satoshimachine.dca_machines m
FROM spirekeeper.dca_machines m
WHERE m.id = d.machine_id
AND m.fiat_code IS NOT NULL
AND m.fiat_code != d.currency
@ -538,7 +538,7 @@ async def m007_add_cassette_configs(db):
"""Add cassette_configs table for operator-driven ATM cassette inventory.
Tracks per-machine cassette state (denomination, count, position) editable
via the satmachineadmin dashboard and published to the ATM as encrypted
via the spirekeeper dashboard and published to the ATM as encrypted
kind-30078 events. See aiolabs/satmachineadmin#29 + lamassu-next#56.
Schema choice: PK (machine_id, denomination) mirrors the ATM-side
@ -556,7 +556,7 @@ async def m007_add_cassette_configs(db):
render them; v2 reconciliation UI consumes them without a migration.
"""
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.cassette_configs (
CREATE TABLE IF NOT EXISTS spirekeeper.cassette_configs (
machine_id TEXT NOT NULL,
denomination INTEGER NOT NULL,
count INTEGER NOT NULL,
@ -600,14 +600,14 @@ async def m008_flip_cassette_configs_pk_to_position(db):
# Probe: does the old PK shape still exist? If state_denomination
# column already exists, m008 already ran — no-op.
await db.fetchone(
"SELECT state_denomination FROM satoshimachine.cassette_configs " "LIMIT 1"
"SELECT state_denomination FROM spirekeeper.cassette_configs " "LIMIT 1"
)
return
except Exception:
pass
await db.execute(f"""
CREATE TABLE IF NOT EXISTS satoshimachine.cassette_configs_new (
CREATE TABLE IF NOT EXISTS spirekeeper.cassette_configs_new (
machine_id TEXT NOT NULL,
position INTEGER NOT NULL,
denomination INTEGER NOT NULL,
@ -630,19 +630,19 @@ async def m008_flip_cassette_configs_pk_to_position(db):
# = current denomination as a best-guess baseline; the next bootstrap
# event re-populates the state_* columns authoritatively.
await db.execute("""
INSERT INTO satoshimachine.cassette_configs_new
INSERT INTO spirekeeper.cassette_configs_new
(machine_id, position, denomination, count,
updated_at, updated_by,
state_denomination, state_count, state_at, state_event_id)
SELECT machine_id, position, denomination, count,
updated_at, updated_by,
denomination, state_count, state_at, state_event_id
FROM satoshimachine.cassette_configs
FROM spirekeeper.cassette_configs
""")
await db.execute("DROP TABLE satoshimachine.cassette_configs")
await db.execute("DROP TABLE spirekeeper.cassette_configs")
await db.execute(
"ALTER TABLE satoshimachine.cassette_configs_new " "RENAME TO cassette_configs"
"ALTER TABLE spirekeeper.cassette_configs_new " "RENAME TO cassette_configs"
)
@ -676,7 +676,7 @@ async def m009_split_fee_fractions_by_direction(db):
operators set via the new UI surface).
- dca_settlements gains fee_mismatch_sats BIGINT NULL records
bitspire-reported fee minus expected per
satmachineadmin's principal-based recompute.
spirekeeper's principal-based recompute.
Phase 1 observability: log + record, never
reject (per coord-log §2026-06-01T07:00Z
lnbits advisory; option A locked).
@ -697,13 +697,13 @@ async def m009_split_fee_fractions_by_direction(db):
]
for table, col, coltype in additions:
try:
await db.fetchone(f"SELECT {col} FROM satoshimachine.{table} LIMIT 1")
await db.fetchone(f"SELECT {col} FROM spirekeeper.{table} LIMIT 1")
# column already present — migration partially-ran previously, skip
continue
except Exception:
pass
await db.execute(
f"ALTER TABLE satoshimachine.{table} ADD COLUMN {col} {coltype}"
f"ALTER TABLE spirekeeper.{table} ADD COLUMN {col} {coltype}"
)
# Backfill + drop the legacy singleton, gated on the column still
@ -711,7 +711,7 @@ async def m009_split_fee_fractions_by_direction(db):
# steps cleanly.
try:
await db.fetchone(
"SELECT super_fee_fraction FROM satoshimachine.super_config LIMIT 1"
"SELECT super_fee_fraction FROM spirekeeper.super_config LIMIT 1"
)
legacy_present = True
except Exception:
@ -724,7 +724,7 @@ async def m009_split_fee_fractions_by_direction(db):
# still at DEFAULT 0).
await db.execute(
"""
UPDATE satoshimachine.super_config
UPDATE spirekeeper.super_config
SET super_cash_in_fee_fraction = super_fee_fraction,
super_cash_out_fee_fraction = super_fee_fraction
WHERE super_cash_in_fee_fraction = 0
@ -733,5 +733,5 @@ async def m009_split_fee_fractions_by_direction(db):
"""
)
await db.execute(
"ALTER TABLE satoshimachine.super_config DROP COLUMN super_fee_fraction"
"ALTER TABLE spirekeeper.super_config DROP COLUMN super_fee_fraction"
)

View file

@ -38,7 +38,7 @@
-- Find duplicate transactions
SELECT transaction_id, COUNT(*) as count,
STRING_AGG(id::text, ', ') as record_ids
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
GROUP BY transaction_id
HAVING COUNT(*) > 1;
```
@ -60,16 +60,16 @@ HAVING COUNT(*) > 1;
-- Step 1: Identify duplicate distributions
SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount,
COUNT(dp.id) as distribution_count
FROM satoshimachine.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
FROM spirekeeper.lamassu_transactions lt
LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = 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
SELECT client_id,
SUM(amount_sats) as total_received,
-- 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)
GROUP BY client_id;
```
@ -94,7 +94,7 @@ if existing:
**Required Database Change**:
```sql
ALTER TABLE satoshimachine.lamassu_transactions
ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
```
@ -117,7 +117,7 @@ ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Check last successful poll
SELECT MAX(created_at) as last_transaction,
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
```
@ -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)
SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at,
c.username, dp.payment_hash
FROM satoshimachine.dca_payments dp
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.dca_payments dp
JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE dp.status != 'completed'
AND dp.created_at < NOW() - INTERVAL '1 hour'
ORDER BY dp.created_at DESC;
@ -207,7 +207,7 @@ ORDER BY dp.created_at DESC;
SELECT
SUM(commission_amount) as total_commission_expected,
-- Manually check actual wallet balance in LNBits
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
```
#### Immediate Response
@ -258,9 +258,9 @@ SELECT
c.remaining_balance,
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
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.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
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_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(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
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.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
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
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;
```
@ -341,11 +341,11 @@ HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE
```sql
-- Get complete transaction history for client
SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE client_id = <client_id>
UNION ALL
SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
WHERE client_id = <client_id>
ORDER BY created_at;
```
@ -361,18 +361,18 @@ ORDER BY created_at;
**Option A: Adjustment Entry** (Recommended)
```sql
-- 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');
-- 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');
```
**Option B: Direct Balance Update** (Use with extreme caution)
```sql
-- ONLY if audit trail is complete and discrepancy is unexplained
UPDATE satoshimachine.dca_clients
UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_balance>,
updated_at = NOW()
WHERE id = <client_id>;
@ -396,11 +396,11 @@ async def daily_reconciliation_check():
**Database Constraints**:
```sql
-- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients
ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Prevent confirmed deposits with zero amount
ALTER TABLE satoshimachine.dca_deposits
ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0);
```
@ -435,7 +435,7 @@ SELECT
-- Calculate differences
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
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
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;
```
@ -485,9 +485,9 @@ SELECT
SUM(lt.base_amount) as total_distributed,
SUM(expected_base) as should_have_distributed,
SUM(expected_base - lt.base_amount) as client_impact
FROM satoshimachine.lamassu_transactions lt
JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.lamassu_transactions lt
JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE -- filter for affected transactions
GROUP BY c.id;
```
@ -505,7 +505,7 @@ GROUP BY c.id;
- Create compensating payments to affected clients:
```sql
-- Add to client balances
UPDATE satoshimachine.dca_clients c
UPDATE spirekeeper.dca_clients c
SET remaining_balance = remaining_balance + adjustment.amount
FROM (
-- Calculate adjustment per client
@ -572,7 +572,7 @@ assert abs(calculated_total - crypto_atoms) <= 1, "Commission calculation error
SELECT
SUM(commission_amount) as expected_total_commission,
-- 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>';
```
@ -607,7 +607,7 @@ curl -X GET https://<lnbits-host>/api/v1/wallet \
SELECT commission_wallet_id,
LEFT(commission_wallet_adminkey, 10) || '...' as key_preview,
updated_at
FROM satoshimachine.lamassu_config
FROM spirekeeper.lamassu_config
ORDER BY updated_at DESC
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,
COUNT(DISTINCT d.id) as deposit_count,
COUNT(DISTINCT p.id) as payment_count
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.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
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id
)
SELECT
@ -813,7 +813,7 @@ SELECT
SUM(commission_amount) as total_commission,
MIN(created_at) as first_transaction,
MAX(created_at) as last_transaction
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
-- 3. Failed/Pending Payments Check
SELECT
@ -822,7 +822,7 @@ SELECT
SUM(amount_sats) as total_amount,
MIN(created_at) as oldest,
MAX(created_at) as newest
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
GROUP BY status
ORDER BY
CASE status
@ -839,7 +839,7 @@ SELECT
status,
created_at,
EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE status = 'pending'
AND created_at < NOW() - INTERVAL '48 hours'
ORDER BY created_at;
@ -855,7 +855,7 @@ SELECT
commission_amount,
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
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC
LIMIT 20;
```
@ -876,15 +876,15 @@ psql -h localhost -U lnbits -d lnbits
2. **Direct Configuration Update**:
```sql
-- Update Lamassu config directly
UPDATE satoshimachine.lamassu_config
UPDATE spirekeeper.lamassu_config
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**:
```sql
-- ONLY in emergency when dashboard unavailable
UPDATE satoshimachine.dca_clients
UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_amount>
WHERE id = <client_id>;
-- MUST document this action in incident log
@ -910,19 +910,19 @@ uv run lnbits
```bash
# Export all DCA-related tables to CSV
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
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
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
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
```
@ -946,9 +946,9 @@ SELECT
dp.amount_sats as client_received,
dp.status as payment_status,
dp.payment_hash
FROM satoshimachine.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.lamassu_transactions lt
LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
ORDER BY lt.created_at DESC, c.username;
```
@ -981,18 +981,18 @@ async def process_lamassu_transaction(txn_data: dict) -> Optional[LamassuTransac
```sql
-- Add unique constraint on transaction_id
ALTER TABLE satoshimachine.lamassu_transactions
ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients
ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Ensure positive amounts
ALTER TABLE satoshimachine.dca_deposits
ALTER TABLE spirekeeper.dca_deposits
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);
```
@ -1196,7 +1196,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
### System Access
**LNBits Admin Dashboard**:
- URL: `https://<your-lnbits-host>/satoshimachine`
- URL: `https://<your-lnbits-host>/spirekeeper`
- Requires superuser authentication
**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
# Direct table access
sqlite3 /path/to/db "SELECT * FROM satoshimachine.<table_name>;"
sqlite3 /path/to/db "SELECT * FROM spirekeeper.<table_name>;"
```
**Log Files**:
@ -1274,7 +1274,7 @@ pkill -f lnbits
```sql
-- Disable automatic polling
UPDATE satoshimachine.lamassu_config
UPDATE spirekeeper.lamassu_config
SET polling_enabled = false;
```
@ -1283,7 +1283,7 @@ SET polling_enabled = false;
```sql
-- All client balances summary
SELECT id, username, remaining_balance, created_at
FROM satoshimachine.dca_clients
FROM spirekeeper.dca_clients
ORDER BY remaining_balance DESC;
```
@ -1291,7 +1291,7 @@ ORDER BY remaining_balance DESC;
```sql
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
LIMIT 10;
```
@ -1299,7 +1299,7 @@ LIMIT 10;
### Failed Payments
```sql
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE status != 'completed'
ORDER BY created_at DESC;
```
@ -1314,19 +1314,19 @@ BACKUP_DIR="emergency_backup_${DATE}"
mkdir -p $BACKUP_DIR
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \
"SELECT * FROM spirekeeper.lamassu_transactions;" \
> ${BACKUP_DIR}/lamassu_transactions.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \
"SELECT * FROM spirekeeper.dca_payments;" \
> ${BACKUP_DIR}/dca_payments.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \
"SELECT * FROM spirekeeper.dca_deposits;" \
> ${BACKUP_DIR}/dca_deposits.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \
"SELECT * FROM spirekeeper.dca_clients;" \
> ${BACKUP_DIR}/dca_clients.csv
echo "Backup complete in ${BACKUP_DIR}/"

View file

@ -38,7 +38,7 @@
-- Find duplicate transactions
SELECT transaction_id, COUNT(*) as count,
STRING_AGG(id::text, ', ') as record_ids
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
GROUP BY transaction_id
HAVING COUNT(*) > 1;
```
@ -60,16 +60,16 @@ HAVING COUNT(*) > 1;
-- Step 1: Identify duplicate distributions
SELECT lt.transaction_id, lt.id, lt.created_at, lt.base_amount,
COUNT(dp.id) as distribution_count
FROM satoshimachine.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
FROM spirekeeper.lamassu_transactions lt
LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = 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
SELECT client_id,
SUM(amount_sats) as total_received,
-- 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)
GROUP BY client_id;
```
@ -94,7 +94,7 @@ if existing:
**Required Database Change**:
```sql
ALTER TABLE satoshimachine.lamassu_transactions
ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
```
@ -117,7 +117,7 @@ ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Check last successful poll
SELECT MAX(created_at) as last_transaction,
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
```
@ -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)
SELECT dp.id, dp.client_id, dp.amount_sats, dp.status, dp.created_at,
c.username, dp.payment_hash
FROM satoshimachine.dca_payments dp
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.dca_payments dp
JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE dp.status != 'completed'
AND dp.created_at < NOW() - INTERVAL '1 hour'
ORDER BY dp.created_at DESC;
@ -207,7 +207,7 @@ ORDER BY dp.created_at DESC;
SELECT
SUM(commission_amount) as total_commission_expected,
-- Manually check actual wallet balance in LNBits
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
```
#### Immediate Response
@ -258,9 +258,9 @@ SELECT
c.remaining_balance,
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
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.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
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_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(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
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.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
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
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;
```
@ -341,11 +341,11 @@ HAVING ABS(c.remaining_balance - (COALESCE(SUM(d.amount), 0) - COALESCE(SUM(CASE
```sql
-- Get complete transaction history for client
SELECT 'DEPOSIT' as type, id, amount, status, created_at, confirmed_at
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE client_id = <client_id>
UNION ALL
SELECT 'PAYMENT' as type, id, amount_sats, status, created_at, NULL
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
WHERE client_id = <client_id>
ORDER BY created_at;
```
@ -361,18 +361,18 @@ ORDER BY created_at;
**Option A: Adjustment Entry** (Recommended)
```sql
-- 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');
-- 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');
```
**Option B: Direct Balance Update** (Use with extreme caution)
```sql
-- ONLY if audit trail is complete and discrepancy is unexplained
UPDATE satoshimachine.dca_clients
UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_balance>,
updated_at = NOW()
WHERE id = <client_id>;
@ -396,11 +396,11 @@ async def daily_reconciliation_check():
**Database Constraints**:
```sql
-- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients
ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Prevent confirmed deposits with zero amount
ALTER TABLE satoshimachine.dca_deposits
ALTER TABLE spirekeeper.dca_deposits
ADD CONSTRAINT positive_deposit CHECK (amount > 0);
```
@ -435,7 +435,7 @@ SELECT
-- Calculate differences
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
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
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;
```
@ -485,9 +485,9 @@ SELECT
SUM(lt.base_amount) as total_distributed,
SUM(expected_base) as should_have_distributed,
SUM(expected_base - lt.base_amount) as client_impact
FROM satoshimachine.lamassu_transactions lt
JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.lamassu_transactions lt
JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
WHERE -- filter for affected transactions
GROUP BY c.id;
```
@ -505,7 +505,7 @@ GROUP BY c.id;
- Create compensating payments to affected clients:
```sql
-- Add to client balances
UPDATE satoshimachine.dca_clients c
UPDATE spirekeeper.dca_clients c
SET remaining_balance = remaining_balance + adjustment.amount
FROM (
-- Calculate adjustment per client
@ -572,7 +572,7 @@ assert abs(calculated_total - crypto_atoms) <= 1, "Commission calculation error
SELECT
SUM(commission_amount) as expected_total_commission,
-- 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>';
```
@ -607,7 +607,7 @@ curl -X GET https://<lnbits-host>/api/v1/wallet \
SELECT commission_wallet_id,
LEFT(commission_wallet_adminkey, 10) || '...' as key_preview,
updated_at
FROM satoshimachine.lamassu_config
FROM spirekeeper.lamassu_config
ORDER BY updated_at DESC
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,
COUNT(DISTINCT d.id) as deposit_count,
COUNT(DISTINCT p.id) as payment_count
FROM satoshimachine.dca_clients c
LEFT JOIN satoshimachine.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
FROM spirekeeper.dca_clients c
LEFT JOIN spirekeeper.dca_deposits d ON c.id = d.client_id AND d.status = 'confirmed'
LEFT JOIN spirekeeper.dca_payments p ON c.id = p.client_id
GROUP BY c.id
)
SELECT
@ -813,7 +813,7 @@ SELECT
SUM(commission_amount) as total_commission,
MIN(created_at) as first_transaction,
MAX(created_at) as last_transaction
FROM satoshimachine.lamassu_transactions;
FROM spirekeeper.lamassu_transactions;
-- 3. Failed/Pending Payments Check
SELECT
@ -822,7 +822,7 @@ SELECT
SUM(amount_sats) as total_amount,
MIN(created_at) as oldest,
MAX(created_at) as newest
FROM satoshimachine.dca_payments
FROM spirekeeper.dca_payments
GROUP BY status
ORDER BY
CASE status
@ -839,7 +839,7 @@ SELECT
status,
created_at,
EXTRACT(EPOCH FROM (NOW() - created_at))/3600 as hours_pending
FROM satoshimachine.dca_deposits
FROM spirekeeper.dca_deposits
WHERE status = 'pending'
AND created_at < NOW() - INTERVAL '48 hours'
ORDER BY created_at;
@ -855,7 +855,7 @@ SELECT
commission_amount,
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
FROM satoshimachine.lamassu_transactions
FROM spirekeeper.lamassu_transactions
ORDER BY created_at DESC
LIMIT 20;
```
@ -876,15 +876,15 @@ psql -h localhost -U lnbits -d lnbits
2. **Direct Configuration Update**:
```sql
-- Update Lamassu config directly
UPDATE satoshimachine.lamassu_config
UPDATE spirekeeper.lamassu_config
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**:
```sql
-- ONLY in emergency when dashboard unavailable
UPDATE satoshimachine.dca_clients
UPDATE spirekeeper.dca_clients
SET remaining_balance = <correct_amount>
WHERE id = <client_id>;
-- MUST document this action in incident log
@ -910,19 +910,19 @@ uv run lnbits
```bash
# Export all DCA-related tables to CSV
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
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
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
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
```
@ -946,9 +946,9 @@ SELECT
dp.amount_sats as client_received,
dp.status as payment_status,
dp.payment_hash
FROM satoshimachine.lamassu_transactions lt
LEFT JOIN satoshimachine.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN satoshimachine.dca_clients c ON dp.client_id = c.id
FROM spirekeeper.lamassu_transactions lt
LEFT JOIN spirekeeper.dca_payments dp ON dp.lamassu_transaction_id = lt.id
LEFT JOIN spirekeeper.dca_clients c ON dp.client_id = c.id
ORDER BY lt.created_at DESC, c.username;
```
@ -981,18 +981,18 @@ async def process_lamassu_transaction(txn_data: dict) -> Optional[LamassuTransac
```sql
-- Add unique constraint on transaction_id
ALTER TABLE satoshimachine.lamassu_transactions
ALTER TABLE spirekeeper.lamassu_transactions
ADD CONSTRAINT unique_transaction_id UNIQUE (transaction_id);
-- Prevent negative balances
ALTER TABLE satoshimachine.dca_clients
ALTER TABLE spirekeeper.dca_clients
ADD CONSTRAINT positive_balance CHECK (remaining_balance >= 0);
-- Ensure positive amounts
ALTER TABLE satoshimachine.dca_deposits
ALTER TABLE spirekeeper.dca_deposits
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);
```
@ -1196,7 +1196,7 @@ grep "wallet.*api\|payment_hash" lnbits.log | tail -50
### System Access
**LNBits Admin Dashboard**:
- URL: `https://<your-lnbits-host>/satoshimachine`
- URL: `https://<your-lnbits-host>/spirekeeper`
- Requires superuser authentication
**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
# Direct table access
sqlite3 /path/to/db "SELECT * FROM satoshimachine.<table_name>;"
sqlite3 /path/to/db "SELECT * FROM spirekeeper.<table_name>;"
```
**Log Files**:
@ -1274,7 +1274,7 @@ pkill -f lnbits
```sql
-- Disable automatic polling
UPDATE satoshimachine.lamassu_config
UPDATE spirekeeper.lamassu_config
SET polling_enabled = false;
```
@ -1283,7 +1283,7 @@ SET polling_enabled = false;
```sql
-- All client balances summary
SELECT id, username, remaining_balance, created_at
FROM satoshimachine.dca_clients
FROM spirekeeper.dca_clients
ORDER BY remaining_balance DESC;
```
@ -1291,7 +1291,7 @@ ORDER BY remaining_balance DESC;
```sql
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
LIMIT 10;
```
@ -1299,7 +1299,7 @@ LIMIT 10;
### Failed Payments
```sql
SELECT * FROM satoshimachine.dca_payments
SELECT * FROM spirekeeper.dca_payments
WHERE status != 'completed'
ORDER BY created_at DESC;
```
@ -1314,19 +1314,19 @@ BACKUP_DIR="emergency_backup_${DATE}"
mkdir -p $BACKUP_DIR
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.lamassu_transactions;" \
"SELECT * FROM spirekeeper.lamassu_transactions;" \
> ${BACKUP_DIR}/lamassu_transactions.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_payments;" \
"SELECT * FROM spirekeeper.dca_payments;" \
> ${BACKUP_DIR}/dca_payments.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_deposits;" \
"SELECT * FROM spirekeeper.dca_deposits;" \
> ${BACKUP_DIR}/dca_deposits.csv
sqlite3 -header -csv /path/to/database.sqlite \
"SELECT * FROM satoshimachine.dca_clients;" \
"SELECT * FROM spirekeeper.dca_clients;" \
> ${BACKUP_DIR}/dca_clients.csv
echo "Backup complete in ${BACKUP_DIR}/"

View file

@ -288,7 +288,7 @@ async def publish_encrypted_kind_30078(
await publish_signed_event(signed)
prefix = f"{log_context}: " if log_context else ""
logger.info(
f"satmachineadmin: {prefix}published kind-30078 to ATM "
f"spirekeeper: {prefix}published kind-30078 to ATM "
f"{recipient_pubkey_hex[:12]}... d-tag={d_tag} "
f"event_id={signed['id'][:12]}..."
)

View file

@ -11,7 +11,7 @@ on a match.
The hook is registered with lnbits' `nostr_transport` at extension-init
time via `register_with_lnbits()`. Until the lnbits side ships
`lnbits.core.services.nostr_transport.register_roster_resolver`, the
registration call lazily-imports + soft-fails so satmachineadmin keeps
registration call lazily-imports + soft-fails so spirekeeper keeps
loading cleanly on any lnbits version.
When the lnbits implementation lands + the satmachine instance has
@ -37,7 +37,7 @@ from loguru import logger
from .crud import get_machine_by_atm_pubkey_hex
_SOURCE_EXTENSION = "satmachineadmin"
_SOURCE_EXTENSION = "spirekeeper"
@dataclass(frozen=True)
@ -111,11 +111,11 @@ def register_with_lnbits() -> bool:
Returns True if the registration landed (lnbits surface available
+ call succeeded), False if soft-failed because lnbits hasn't
shipped `register_roster_resolver` yet that's the expected
state until the path-B lnbits PR lands. Either way satmachineadmin
state until the path-B lnbits PR lands. Either way spirekeeper
boots cleanly; only the routing-via-roster behavior is gated on
the lnbits side being present.
Called once from `satmachineadmin_start()`. Idempotent on the
Called once from `spirekeeper_start()`. Idempotent on the
lnbits side per their 15:15Z spec ("re-registration on extension
reload replaces cleanly").
"""
@ -125,7 +125,7 @@ def register_with_lnbits() -> bool:
)
except ImportError:
logger.info(
"satmachineadmin: nostr-transport roster-resolver hook not "
"spirekeeper: nostr-transport roster-resolver hook not "
"available on this lnbits version (pre-path-B); ATM-npub "
"routing falls through to lnbits' default auto-account-from-"
"npub behaviour. See aiolabs/satmachineadmin#20 / coord-log "
@ -134,7 +134,7 @@ def register_with_lnbits() -> bool:
return False
register_roster_resolver(_SOURCE_EXTENSION, resolve)
logger.info(
f"satmachineadmin: registered '{_SOURCE_EXTENSION}' roster "
f"spirekeeper: registered '{_SOURCE_EXTENSION}' roster "
"resolver with lnbits nostr-transport — inbound kind-21000 "
"from a registered ATM npub will route to the operator's wallet "
"directly. (Behavior gated server-side by "

View file

@ -1,7 +1,7 @@
[tool.poetry]
name = "satmachineadmin"
version = "0.0.4"
description = "Eightball is a simple API that allows you to create a random number generator."
name = "spirekeeper"
version = "0.1.0"
description = "bitSpire admin — Lightning DCA + Lamassu ATM operator administration for LNbits"
authors = ["benarc", "dni <dni@lnbits.com>"]
[tool.poetry.dependencies]

View file

@ -1,6 +1,6 @@
// Satoshi Machine v2 — operator dashboard (P9a foundation).
//
// Vue 3 + Quasar UMD app. Talks to the v2 satmachineadmin REST surface
// Vue 3 + Quasar UMD app. Talks to the v2 spirekeeper REST surface
// (machines / clients / deposits / settlements / commission-splits /
// super-config). All endpoints are operator-scoped via the LNbits session.
//
@ -12,7 +12,7 @@
// - For pale backgrounds (bg-*-1), pair with explicit dark text class
// so dark-mode users don't get unreadable white-on-cream.
const API = '/satmachineadmin/api/v1/dca'
const API = '/spirekeeper/api/v1/dca'
const SUPER_FEE_PATH = `${API}/super-config`
const MACHINES_PATH = `${API}/machines`
const SETTLEMENTS_PATH = `${API}/settlements`
@ -198,7 +198,7 @@ window.app = Vue.createApp({
settlements: [],
// Cassettes sub-tab state (#29 v1) — see openCassettePublishConfirm /
// submitCassettePublish methods + the cassettes panel in
// templates/satmachineadmin/index.html.
// templates/spirekeeper/index.html.
activeTab: 'settlements',
cassetteEdits: [], // editable working copy of cassette_configs rows
cassettesPristine: [], // last-known-clean snapshot for revert

View file

@ -45,7 +45,7 @@ from .crud import (
from .distribution import process_settlement
from .models import CreateDcaSettlementData, Machine
LISTENER_NAME = "ext_satmachineadmin"
LISTENER_NAME = "ext_spirekeeper"
# Holds strong refs to in-flight distribution tasks so Python's GC doesn't
# collect them mid-flight (asyncio.create_task only weakly references its
@ -58,7 +58,7 @@ async def wait_for_paid_invoices() -> None:
invoice_queue: asyncio.Queue = asyncio.Queue()
register_invoice_listener(invoice_queue, LISTENER_NAME)
logger.info(
"satmachineadmin v2: invoice listener registered as "
"spirekeeper v2: invoice listener registered as "
f"`{LISTENER_NAME}` — waiting for bitSpire settlements."
)
while True:
@ -67,7 +67,7 @@ async def wait_for_paid_invoices() -> None:
await _handle_payment(payment)
except Exception as exc: # listener must never die
logger.error(
f"satmachineadmin: error handling payment "
f"spirekeeper: error handling payment "
f"{payment.payment_hash[:12]}...: {exc}"
)
@ -169,12 +169,12 @@ async def _handle_payment(payment: Payment) -> None:
settlement = await create_settlement_idempotent(data, initial_status="pending")
if settlement is None:
logger.error(
f"satmachineadmin: failed to insert settlement for "
f"spirekeeper: failed to insert settlement for "
f"payment_hash={payment.payment_hash[:12]}..."
)
return
logger.info(
f"satmachineadmin: landed settlement {settlement.id} for "
f"spirekeeper: landed settlement {settlement.id} for "
f"machine={machine.machine_npub[:12]}... "
f"wire={data.wire_sats}sats principal={data.principal_sats}sats "
f"fee={data.fee_sats}sats "
@ -224,12 +224,12 @@ async def _record_rejected(payment: Payment, machine: Machine, exc: Exception) -
)
if rejected is None:
logger.error(
f"satmachineadmin: failed to insert rejected settlement for "
f"spirekeeper: failed to insert rejected settlement for "
f"payment_hash={payment.payment_hash[:12]}..."
)
return
logger.error(
f"satmachineadmin: rejected settlement {rejected.id} "
f"spirekeeper: rejected settlement {rejected.id} "
f"(machine={machine.machine_npub[:12]}..., "
f"payment_hash={payment.payment_hash[:12]}...): {exc}"
)
@ -245,7 +245,7 @@ async def _record_rejected(payment: Payment, machine: Machine, exc: Exception) -
# upserts cassette_configs via apply_bootstrap_state.
#
# v1 = one-shot per machine (ATM's meta.bootstrapPublishedAt makes the
# publish idempotent on ATM-side restart; satmachineadmin's apply_bootstrap_
# publish idempotent on ATM-side restart; spirekeeper's apply_bootstrap_
# state dedups on state_event_id for relay re-delivery).
#
# v2 (separate issue) = continuous reverse-channel consumer with a
@ -258,7 +258,7 @@ async def _record_rejected(payment: Payment, machine: Machine, exc: Exception) -
# websocket. The relay manager is the same singleton publish_to_atm uses,
# so add_subscription registers a filter against the same relay pool.
CASSETTE_BOOTSTRAP_SUB_ID = "satmachineadmin-cassette-bootstrap"
CASSETTE_BOOTSTRAP_SUB_ID = "spirekeeper-cassette-bootstrap"
_CASSETTE_POLL_INTERVAL_S = 2.0
_CASSETTE_BACKOFF_S = 30.0 # when nostrclient isn't installed yet
@ -280,7 +280,7 @@ async def wait_for_cassette_state_events() -> None:
- apply_bootstrap_state errors log + skip
"""
logger.info(
"satmachineadmin v2: cassette bootstrap consumer starting "
"spirekeeper v2: cassette bootstrap consumer starting "
f"(sub_id={CASSETTE_BOOTSTRAP_SUB_ID})"
)
current_filter_key: str | None = None
@ -290,7 +290,7 @@ async def wait_for_cassette_state_events() -> None:
await asyncio.sleep(_CASSETTE_POLL_INTERVAL_S)
except _NostrclientUnavailable:
logger.warning(
"satmachineadmin: nostrclient extension not installed; "
"spirekeeper: nostrclient extension not installed; "
f"cassette bootstrap consumer sleeping {_CASSETTE_BACKOFF_S}s "
"before retry. Install + activate nostrclient on this "
"LNbits instance."
@ -299,7 +299,7 @@ async def wait_for_cassette_state_events() -> None:
await asyncio.sleep(_CASSETTE_BACKOFF_S)
except Exception as exc: # listener must never die
logger.error(
f"satmachineadmin: cassette consumer loop error (continuing): " f"{exc}"
f"spirekeeper: cassette consumer loop error (continuing): " f"{exc}"
)
await asyncio.sleep(_CASSETTE_POLL_INTERVAL_S)
@ -346,13 +346,13 @@ async def _cassette_consumer_tick(current_filter_key: str | None) -> str:
CASSETTE_BOOTSTRAP_SUB_ID, filters # type: ignore[arg-type]
)
logger.info(
"satmachineadmin: (re)registered cassette bootstrap "
"spirekeeper: (re)registered cassette bootstrap "
f"subscription with {len(d_tags)} d-tag(s)"
)
else:
nostr_client.relay_manager.close_subscription(CASSETTE_BOOTSTRAP_SUB_ID)
logger.info(
"satmachineadmin: no active machines; closed cassette "
"spirekeeper: no active machines; closed cassette "
"bootstrap subscription"
)
@ -368,7 +368,7 @@ async def _cassette_consumer_tick(current_filter_key: str | None) -> str:
)
except Exception as exc:
logger.warning(
f"satmachineadmin: cassette state event handler "
f"spirekeeper: cassette state event handler "
f"failed (skipping): {exc}"
)
@ -419,14 +419,14 @@ async def _handle_cassette_state_event(
event_obj = event_raw
else:
logger.warning(
f"satmachineadmin: cassette event of unexpected type "
f"spirekeeper: cassette event of unexpected type "
f"{type(event_raw).__name__}; skipping"
)
return
if not verify_event(event_obj):
logger.warning(
f"satmachineadmin: cassette state event sig verify failed "
f"spirekeeper: cassette state event sig verify failed "
f"(id={event_obj.get('id', '?')[:12]}...)"
)
return
@ -437,7 +437,7 @@ async def _handle_cassette_state_event(
# Unknown sender — could be relay noise or an attacker. Don't
# treat as our problem.
logger.warning(
f"satmachineadmin: cassette state event from unknown ATM "
f"spirekeeper: cassette state event from unknown ATM "
f"pubkey {sender_pubkey[:12]}... (not in dca_machines); "
"skipping"
)
@ -448,7 +448,7 @@ async def _handle_cassette_state_event(
except CassetteTransportError as exc:
# OperatorIdentityMissing / SignerUnavailable — log + skip.
logger.warning(
f"satmachineadmin: can't resolve signer for operator "
f"spirekeeper: can't resolve signer for operator "
f"{machine.operator_user_id[:8]}... (machine {machine.id}): "
f"{exc}"
)
@ -458,13 +458,13 @@ async def _handle_cassette_state_event(
payload = await decrypt_and_parse_state_event(event_obj, account, signer)
except CassetteEventTransientError as exc:
logger.info(
f"satmachineadmin: cassette state event for machine {machine.id} "
f"spirekeeper: cassette state event for machine {machine.id} "
f"hit a transient signer error (will retry next poll): {exc}"
)
return
except CassetteEventDecodeError as exc:
logger.warning(
f"satmachineadmin: cassette state event decode failed for "
f"spirekeeper: cassette state event decode failed for "
f"machine {machine.id} (id={event_obj.get('id', '?')[:12]}...): "
f"{exc}"
)
@ -479,12 +479,12 @@ async def _handle_cassette_state_event(
)
if applied:
logger.info(
f"satmachineadmin: applied bootstrap state event {event_id[:12]}... "
f"spirekeeper: applied bootstrap state event {event_id[:12]}... "
f"to machine {machine.id} ({len(payload.positions)} cassettes)"
)
else:
# Replay: event_id already on file. Normal on relay reconnect.
logger.debug(
f"satmachineadmin: cassette state event {event_id[:12]}... "
f"spirekeeper: cassette state event {event_id[:12]}... "
f"already applied to machine {machine.id} (replay no-op)"
)

View file

@ -3,7 +3,7 @@
{% block scripts %}
{{ window_vars(user) }}
<script src="{{ static_url_for('satmachineadmin/static', path='js/index.js') }}"></script>
<script src="{{ static_url_for('spirekeeper/static', path='js/index.js') }}"></script>
{% endblock %}
{% block page %}

View file

@ -1,5 +1,5 @@
"""
Pytest configuration for the satmachineadmin extension test suite.
Pytest configuration for the spirekeeper extension test suite.
Provides a `loguru_capture` fixture for tests that need to verify
loguru WARN/ERROR side-effects. Loguru attaches its default sink to

View file

@ -210,7 +210,7 @@ class TestShouldApplyBootstrapState:
def test_skips_when_existing_event_id_matches(self):
"""The same bootstrap event re-delivered after a relay reconnect
or satmachineadmin restart should no-op, not re-upsert the same
or spirekeeper restart should no-op, not re-upsert the same
rows (which would clobber any operator edits since)."""
assert _should_apply_bootstrap_state("same-event", "same-event") is False

View file

@ -10,7 +10,7 @@ auto-account wallet.
The guard refuses to register a machine whose npub matches any LNbits
operator account's `accounts.pubkey`, so this state cannot be entered
through the satmachineadmin UI in the first place.
through the spirekeeper UI in the first place.
Monkeypatches `views_api.get_account_by_pubkey` to avoid needing a live
LNbits DB; this matches the assertion-style of tests/test_nostr_attribution

View file

@ -7,7 +7,7 @@ Each settlement records:
fee_mismatch_sats = bitspire_fee_sats - (platform_fee_sats + operator_fee_sats)
Positive = bitspire over-reported (claimed more fee than satmachineadmin
Positive = bitspire over-reported (claimed more fee than spirekeeper
recomputed against principal). Negative = bitspire under-reported.
Zero = exact match.
@ -116,7 +116,7 @@ class TestFeeMismatchSatsRecording:
"""Real-world Phase-1 scenario before Layer 3 (lamassu-next#57)
ships: ATM hardcodes 7.77% cash-out; operator configures 5%
operator + 3% super = 8% total. Bitspire reports
100_000 * 0.0777 = 7_770 sats; satmachineadmin recomputes 8_000.
100_000 * 0.0777 = 7_770 sats; spirekeeper recomputes 8_000.
Delta is large and visible for triage; behavior unchanged."""
machine = _machine(op_out=0.05)
super_cfg = _super_config(out_frac=0.03)

View file

@ -1,6 +1,6 @@
"""
Tests for `nostr_transport_roster.resolve` the lookup function
satmachineadmin hands lnbits' nostr-transport via
spirekeeper hands lnbits' nostr-transport via
`register_roster_resolver` (path-B wallet-routing fix, #20 /
coord-log 2026-05-31T15:25Z).
@ -73,7 +73,7 @@ def test_resolve_known_atm_returns_route_hit(monkeypatch):
assert result is not None
assert result.operator_user_id == "op-123"
assert result.wallet_id == "wallet-abc"
assert result.source_extension == "satmachineadmin"
assert result.source_extension == "spirekeeper"
assert captured["pubkey_hex"] == _ATM_PUB_HEX
@ -141,7 +141,7 @@ def test_resolve_raises_on_malformed_input(monkeypatch):
def test_register_with_lnbits_soft_fails_without_hook(monkeypatch):
"""Until the lnbits-side path-B PR lands, the registration call
must soft-fail cleanly (returns False, no exception) so
satmachineadmin keeps booting on every lnbits version."""
spirekeeper keeps booting on every lnbits version."""
real_import = (
__builtins__["__import__"]
if isinstance(__builtins__, dict)

View file

@ -10,16 +10,16 @@ from lnbits.core.models import User
from lnbits.decorators import check_user_exists
from lnbits.helpers import template_renderer
satmachineadmin_generic_router = APIRouter()
spirekeeper_generic_router = APIRouter()
def satmachineadmin_renderer():
return template_renderer(["satmachineadmin/templates"])
def spirekeeper_renderer():
return template_renderer(["spirekeeper/templates"])
@satmachineadmin_generic_router.get("/", response_class=HTMLResponse)
@spirekeeper_generic_router.get("/", response_class=HTMLResponse)
async def index(req: Request, user: User = Depends(check_user_exists)):
return satmachineadmin_renderer().TemplateResponse(
"satmachineadmin/index.html",
return spirekeeper_renderer().TemplateResponse(
"spirekeeper/index.html",
{"request": req, "user": user.json()},
)

View file

@ -94,7 +94,7 @@ from .models import (
UpsertCassetteConfigData,
)
satmachineadmin_api_router = APIRouter()
spirekeeper_api_router = APIRouter()
async def _assert_wallet_owned_by(wallet_id: str, user_id: str) -> None:
@ -254,7 +254,7 @@ async def _assert_super_config_cap_safe(
# =============================================================================
@satmachineadmin_api_router.post("/api/v1/dca/machines", response_model=Machine)
@spirekeeper_api_router.post("/api/v1/dca/machines", response_model=Machine)
async def api_create_machine(
data: CreateMachineData, user: User = Depends(check_user_exists)
) -> Machine:
@ -274,14 +274,14 @@ async def api_create_machine(
return machine
@satmachineadmin_api_router.get("/api/v1/dca/machines", response_model=list[Machine])
@spirekeeper_api_router.get("/api/v1/dca/machines", response_model=list[Machine])
async def api_list_machines(
user: User = Depends(check_user_exists),
) -> list[Machine]:
return await get_machines_for_operator(user.id)
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/machines/{machine_id}", response_model=Machine
)
async def api_get_machine(
@ -293,7 +293,7 @@ async def api_get_machine(
return machine
@satmachineadmin_api_router.put(
@spirekeeper_api_router.put(
"/api/v1/dca/machines/{machine_id}", response_model=Machine
)
async def api_update_machine(
@ -341,7 +341,7 @@ async def api_update_machine(
return updated
@satmachineadmin_api_router.delete(
@spirekeeper_api_router.delete(
"/api/v1/dca/machines/{machine_id}", status_code=HTTPStatus.NO_CONTENT
)
async def api_delete_machine(
@ -379,7 +379,7 @@ async def _client_owned_by(client_id: str, user_id: str) -> DcaClient:
return client
@satmachineadmin_api_router.post("/api/v1/dca/clients", response_model=DcaClient)
@spirekeeper_api_router.post("/api/v1/dca/clients", response_model=DcaClient)
async def api_create_client(
data: CreateDcaClientData, user: User = Depends(check_user_exists)
) -> DcaClient:
@ -388,7 +388,7 @@ async def api_create_client(
return await create_dca_client(data)
@satmachineadmin_api_router.get("/api/v1/dca/clients", response_model=list[DcaClient])
@spirekeeper_api_router.get("/api/v1/dca/clients", response_model=list[DcaClient])
async def api_list_clients(
machine_id: str | None = None,
user: User = Depends(check_user_exists),
@ -402,7 +402,7 @@ async def api_list_clients(
return await get_dca_clients_for_machine(machine_id)
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/clients/{client_id}", response_model=DcaClient
)
async def api_get_client(
@ -411,7 +411,7 @@ async def api_get_client(
return await _client_owned_by(client_id, user.id)
@satmachineadmin_api_router.put(
@spirekeeper_api_router.put(
"/api/v1/dca/clients/{client_id}", response_model=DcaClient
)
async def api_update_client(
@ -426,7 +426,7 @@ async def api_update_client(
return updated
@satmachineadmin_api_router.delete(
@spirekeeper_api_router.delete(
"/api/v1/dca/clients/{client_id}", status_code=HTTPStatus.NO_CONTENT
)
async def api_delete_client(
@ -436,7 +436,7 @@ async def api_delete_client(
await delete_dca_client(client_id)
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/clients/{client_id}/balance",
response_model=ClientBalanceSummary,
)
@ -450,7 +450,7 @@ async def api_get_client_balance(
return summary
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/clients/{client_id}/settle", response_model=DcaPayment
)
async def api_settle_client_balance(
@ -498,7 +498,7 @@ async def _deposit_owned_by(deposit_id: str, user_id: str) -> DcaDeposit:
return deposit
@satmachineadmin_api_router.post("/api/v1/dca/deposits", response_model=DcaDeposit)
@spirekeeper_api_router.post("/api/v1/dca/deposits", response_model=DcaDeposit)
async def api_create_deposit(
data: CreateDepositData, user: User = Depends(check_user_exists)
) -> DcaDeposit:
@ -531,7 +531,7 @@ async def api_create_deposit(
return await create_deposit(user.id, data, currency=machine.fiat_code)
@satmachineadmin_api_router.get("/api/v1/dca/deposits", response_model=list[DcaDeposit])
@spirekeeper_api_router.get("/api/v1/dca/deposits", response_model=list[DcaDeposit])
async def api_list_deposits(
client_id: str | None = None,
user: User = Depends(check_user_exists),
@ -544,7 +544,7 @@ async def api_list_deposits(
return await get_deposits_for_operator(user.id)
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/deposits/{deposit_id}", response_model=DcaDeposit
)
async def api_get_deposit(
@ -553,7 +553,7 @@ async def api_get_deposit(
return await _deposit_owned_by(deposit_id, user.id)
@satmachineadmin_api_router.put(
@spirekeeper_api_router.put(
"/api/v1/dca/deposits/{deposit_id}", response_model=DcaDeposit
)
async def api_update_deposit(
@ -573,7 +573,7 @@ async def api_update_deposit(
return updated
@satmachineadmin_api_router.put(
@spirekeeper_api_router.put(
"/api/v1/dca/deposits/{deposit_id}/status", response_model=DcaDeposit
)
async def api_update_deposit_status(
@ -588,7 +588,7 @@ async def api_update_deposit_status(
return updated
@satmachineadmin_api_router.delete(
@spirekeeper_api_router.delete(
"/api/v1/dca/deposits/{deposit_id}", status_code=HTTPStatus.NO_CONTENT
)
async def api_delete_deposit(
@ -608,7 +608,7 @@ async def api_delete_deposit(
# =============================================================================
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/settlements", response_model=list[DcaSettlement]
)
async def api_list_settlements(
@ -617,7 +617,7 @@ async def api_list_settlements(
return await get_settlements_for_operator(user.id)
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/machines/{machine_id}/settlements",
response_model=list[DcaSettlement],
)
@ -637,7 +637,7 @@ async def api_list_settlements_for_machine(
# order).
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/settlements/stuck", response_model=StuckSettlementsResponse
)
async def api_list_stuck_settlements(
@ -668,7 +668,7 @@ async def api_list_stuck_settlements(
)
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/settlements/{settlement_id}", response_model=DcaSettlement
)
async def api_get_settlement(
@ -683,7 +683,7 @@ async def api_get_settlement(
return settlement
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/partial-dispense",
response_model=DcaSettlement,
)
@ -718,7 +718,7 @@ async def api_partial_dispense(
raise HTTPException(HTTPStatus.BAD_REQUEST, str(exc)) from exc
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/force-reset",
response_model=DcaSettlement,
)
@ -768,7 +768,7 @@ async def api_force_reset_settlement(
return updated
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/retry",
response_model=DcaSettlement,
)
@ -821,7 +821,7 @@ async def api_retry_settlement(
return after if after is not None else updated
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/settlements/{settlement_id}/notes",
response_model=DcaSettlement,
)
@ -854,7 +854,7 @@ async def api_append_settlement_note(
# =============================================================================
@satmachineadmin_api_router.get("/api/v1/dca/payments", response_model=list[DcaPayment])
@spirekeeper_api_router.get("/api/v1/dca/payments", response_model=list[DcaPayment])
async def api_list_payments(
leg_type: str | None = None,
user: User = Depends(check_user_exists),
@ -869,7 +869,7 @@ async def api_list_payments(
# =============================================================================
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/commission-splits", response_model=list[CommissionSplit]
)
async def api_get_commission_splits(
@ -889,7 +889,7 @@ async def api_get_commission_splits(
return await get_commission_splits(user.id, None)
@satmachineadmin_api_router.put(
@spirekeeper_api_router.put(
"/api/v1/dca/commission-splits", response_model=list[CommissionSplit]
)
async def api_replace_commission_splits(
@ -905,7 +905,7 @@ async def api_replace_commission_splits(
return await replace_commission_splits(user.id, data.machine_id, data.legs)
@satmachineadmin_api_router.delete(
@spirekeeper_api_router.delete(
"/api/v1/dca/commission-splits",
status_code=HTTPStatus.NO_CONTENT,
)
@ -927,7 +927,7 @@ async def api_delete_commission_splits(
# =============================================================================
@satmachineadmin_api_router.get("/api/v1/dca/super-config", response_model=SuperConfig)
@spirekeeper_api_router.get("/api/v1/dca/super-config", response_model=SuperConfig)
async def api_get_super_config(
_user: User = Depends(check_user_exists),
) -> SuperConfig:
@ -940,7 +940,7 @@ async def api_get_super_config(
return config
@satmachineadmin_api_router.put("/api/v1/dca/super-config", response_model=SuperConfig)
@spirekeeper_api_router.put("/api/v1/dca/super-config", response_model=SuperConfig)
async def api_update_super_config(
data: UpdateSuperConfigData,
_user: User = Depends(check_super_user),
@ -988,7 +988,7 @@ async def api_update_super_config(
# fields per row are denomination and count.
@satmachineadmin_api_router.get(
@spirekeeper_api_router.get(
"/api/v1/dca/machines/{machine_id}/cassettes",
response_model=list[CassetteConfig],
)
@ -1003,7 +1003,7 @@ async def api_list_machine_cassettes(
return await list_cassette_configs_for_machine(machine_id)
@satmachineadmin_api_router.post(
@spirekeeper_api_router.post(
"/api/v1/dca/machines/{machine_id}/cassettes/publish",
response_model=list[CassetteConfig],
)
@ -1049,7 +1049,7 @@ async def api_publish_machine_cassettes(
"No cassette_configs rows exist for this machine yet — "
"waiting for the ATM's bootstrap state event. Power on the "
"ATM and confirm it has reached the configured relay; "
"satmachineadmin will auto-populate cassette_configs on "
"spirekeeper will auto-populate cassette_configs on "
"receipt."
),
)
@ -1068,7 +1068,7 @@ async def api_publish_machine_cassettes(
)
# Apply each per-row edit so the operator-believed state on
# satmachineadmin reflects the published payload, even if the ATM
# spirekeeper reflects the published payload, even if the ATM
# ack lands later (v2). updated_by audit-stamps the operator user id.
for pos, row in payload.positions.items():
updated = await update_cassette_config(