feat: operator-configurable fee architecture (super% + per-machine operator%) — replaces bitspire-hardcoded fee #37
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Tracking issue
Replaces the current bitspire-hardcoded fee model with operator-configurable fees split between the lnbits super-admin and the per-machine operator, both calculated against the principal amount.
Architectural intent
super_cash_in_fee_fractionandsuper_cash_out_fee_fraction.operator_cash_in_fee_fractionandoperator_cash_out_fee_fraction.What's wrong today (2026-05-31)
Bug 1 —
super_fee_fractionmath is wrong, super is under-paid ~13× per cashoutbitspire.py:256-257interpretssuper_fee_fractionas "fraction of the total fee bitspire sent" rather than "fraction of principal":Concrete demonstration from tonight's settlement
azSz9uEcUaiFHZaJ8eqS5S:super_fee_fractionconfiguredSame configured rate (5%); super is being paid 13× less than the operator setting implies. This has been the behavior since the bitspire wire-shape was wired up; SuperConfig has always been under-applied this way.
Bug 2 — bitspire decides the total fee, not satmachineadmin
Per
apps/machine/src/stores/atm.ts:290-291inaiolabs/lamassu-next:Hardcoded literal constants. No env var, no Nostr config event, no satmachineadmin-side push. Every Sintra that runs the current bitspire build charges those fees regardless of who owns it. Operator cannot configure their fee without rebuilding the firmware.
Gap 3 — no per-machine
operator_fee_fractionfields existMachine,CreateMachineData,UpdateMachineDatainmodels.pycarry no operator fee fields. The implementation has nowhere to store the operator's Y% even if we added it to the UI.Three layers, sequenceable
aiolabs/satmachineadminoperator_cash_in_fee_fraction+operator_cash_out_fee_fraction+ principal-based split math + optional fee_sats validation. Closes Bug 1 standalone.aiolabs/satmachineadminbitspire-fees:<atm_pubkey>kind-30078 events on machine create / fee edit / super-config change. Partners withaiolabs/lamassu-next#57— wire-format must be agreed jointly.aiolabs/lamassu-next#57aiolabs/lamassu-nextcashInFeeFraction/cashOutFeeFraction; fail-closed on no-config. Closes Bug 2 jointly with #39.Layer 1 is fully autonomous and corrects Bug 1 (under-payment) on its own. Layers 2+3 close the loop so operators can actually configure their fee end-to-end (Bug 2) and the super-admin's X% is canonical rather than dependent on bitspire's chosen total.
Resolved design decisions
SuperConfigandMachineboth grow paired fields (*_cash_in_fee_fraction,*_cash_out_fee_fraction). Wire-format carriescash_in_fee_fractionandcash_out_fee_fractionas independent values. Rationale: cash-in consumes cash inventory, cash-out consumes BTC inventory — different economics per direction for both super and operator. Locked 2026-05-31 by Padreug.Still-open design questions
Captured in #38 / #39 /
aiolabs/lamassu-next#57:enforce_fee_matchdefault — once Layers 2+3 ship, should satmachineadmin reject settlements whosefee_satsdoesn't matchprincipal × (super + operator)± tolerance? Recommend "yes, default-on" once the wire-format reaches the ATM reliably.Future-proofing for promos (out of scope for this work; design considerations only)
User-targeted promos (per-customer fee discounts / promotional rates) are explicitly not in scope for this issue or its sub-issues, but the architectural choices made now should not paint us into a corner. Key shape decisions that preserve promo extensibility:
schema_versionin the wire-format payload (#39, #57) — gives us a version-gated upgrade path for any future field additions (e.g.discounts: [{npub, fraction, expires_at}, ...]) without breaking consumers on older schemas. Bitspire treats unknown fields as ignorable; satmachineadmin can publish v2 events to v2-aware ATMs.applies_to: "super" | "operator" | "both".Payment.extra.nostr_sender_pubkeyis already there (from path B) — promos can later key targeting on customer's npub without any new metadata plumbing.discount_satsis a single ADD COLUMN, no schema rework. Don't pre-add the column now (don't speculate on shape); just keep the per-component recording discipline.These are shape decisions baked into the current layers. No code in this fee work attempts to implement promos.
Prior-art reference — membership/discount system from legacy aiolabs/lamassu-server
A fully-fleshed-out membership/discount design exists from the pre-Nostr-pivot era at
aiolabs/lamassu-server#1(filed 2026-01-02) — tiered memberships (Bronze/Silver/Gold/Platinum), QR-code-scan UX, Lightning Address resolution, audit-log table, GraphQL admin API. The full plan doc is parked at~/dev/coordination/membership-discount-plan-legacy-reference.mdwith a header noting the Nostr-native mapping.When the promo workstream scopes, the legacy plan is a starting point — most of the UX and data-shape thinking is reusable; the Nostr-native delta is:
external_user_idfieldbitspire-memberships:<atm_pubkey>(mirrors cassette + fee-config patterns)kind:10063zap targets /kind:0profile metadata — already solved in the Nostr stackmembership_usageaudit tablePayment.extra.nostr_sender_pubkeyon every settlement, free from path BThe current fee architecture's
schema_version-gated wire-format + principal-based math + per-component settlement recording are intentionally shape-compatible with this. No code added in #37/#38/#39/#57 attempts to implement it.Operational note
Settlements completed before Layer 1 lands are recorded with the under-paid super share. Operationally, that's a Padreug-side reconcile decision (recompute the historical splits and back-transfer, or write off as known-prior-behavior). Not a code change.
Cross-refs
19:35Zarchived at~/dev/coordination/archive/2026-05-31-path-b-shipped.mdparse_settlementmath:bitspire.py:217-263models.py:411-433(SuperConfig,UpdateSuperConfigData)aiolabs/lamassu-nextapps/machine/src/stores/atm.ts:290-291aiolabs/satmachineadmin#29(PR #30),aiolabs/lamassu-next#56aiolabs/lamassu-server#1+~/dev/coordination/membership-discount-plan-legacy-reference.mdoperator_fee_fraction+ principal-based split math (closes super under-payment) #38Closing — joint smoke succeeded 2026-06-01
All three layers shipped + validated end-to-end on the Sintra with a physical cash-out.
Status
aiolabs/lamassu-next#579bdb933on the SintraBoth bugs closed
cashInFeeFraction = 0.0333/cashOutFeeFraction = 0.0777cash_in_fee_fraction = 0.08/cash_out_fee_fraction = 0.08via NIP-44-v2-encrypted kind-30078 event, applied reactively without restartJoint smoke result (settlement
T7yoh8BuEeXer79J9X9Swh)Open design questions — all answered during the work
07:22Z)enforce_fee_matchdefault → Phase 1 ship as observability-only viafee_mismatch_satscolumn; Phase 2 (reject) lands as follow-up once data justifies the threshold (coord-log §07:00Z)07:22Z)Wire-format documented decision arc
Worth noting one architectural reversal in the coord-log thread: bitspire originally proposed threading
componentsthrough XState into per-tx audit columns on the ATM (§07:30Z); sat + lnbits endorsed before Padreug's "should the machine be dumb" cut through the ratchet, reverting it (§07:56Z). Final shape:dca_settlementsis the canonical per-tx audit substrate; bitspire's[Fees] applied …journalctl line is the forensic floor. Saved as a procedural memory entry (feedback_endorsement_ratchet.md) for future cross-session coordination.Future-proofing preserved
schema_versionin wire payload — version-gated upgrade path openPayment.extra.nostr_sender_pubkey— promo targeting plumbing already exists from path BADD COLUMN discount_satsaway from promo supportCleanup follow-ups (none blocking)
aiolabs/lamassu-next):wss://relay.aiolabs.devhardcoded default points at NXDOMAIN — Padreug policy call on whether to stand the relay up or move to QR-code seeding (#41's territory)aiolabs/lamassu-next): dropVITE_LNBITS_HTTP_URL+ boot echo + manual encodeLnurl composition now thataiolabs/withdraw e9d911epopulateslink.lnurlupstreamrepublish_operator_configshelper for LocalSigner→RemoteBunkerSigner migration cascade — implementable on top of fee_transport + cassette_transport primitivesenforce_fee_match: settlement-reject on out-of-tolerance once data justifies the thresholdClosing the parent.
refs: coord-log §
2026-06-01T18:20Z(joint-smoke success entry), PR #42, PR #43,aiolabs/lamassu-next@9bdb933(Layer 3 deploy), settlementT7yoh8BuEeXer79J9X9Swh