feat: Layer 1 — per-machine operator_fee_fraction + principal-based split math (closes super under-payment) #38
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?
Layer 1 of the operator-configurable fee architecture (parent: #37).
Fully autonomous on the satmachineadmin side — no bitspire coordination required. Corrects the under-payment of the super fee on its own; Layer 2 and Layer 3 are independent improvements that close the operator-configuration loop.
Locked design decisions (per #37)
SuperConfigandMachinelevels. BothSuperConfiggrowssuper_cash_in_fee_fraction+super_cash_out_fee_fraction;Machinegrowsoperator_cash_in_fee_fraction+operator_cash_out_fee_fraction.What ships
1. Schema migrations — add the four fee fields
SuperConfig(singleton):super_cash_in_fee_fraction DECIMAL(5,4) NOT NULL DEFAULT 0,super_cash_out_fee_fraction DECIMAL(5,4) NOT NULL DEFAULT 0. Existingsuper_fee_fractioncolumn: deprecated in this migration (kept as nullable for one release cycle so a downgrade doesn't crash; remove in a follow-up). On migrate-up, backfillsuper_cash_in_fee_fraction = super_cash_out_fee_fraction = super_fee_fractionso the live config preserves its current intent.Machine(per-row):operator_cash_in_fee_fraction DECIMAL(5,4) NOT NULL DEFAULT 0,operator_cash_out_fee_fraction DECIMAL(5,4) NOT NULL DEFAULT 0. Existing rows get0via DEFAULT clause.m00N_split_fee_fractions_by_directioninmigrations_fork.pyper the existing fork-migrations pattern (idempotentALTER TABLE … ADD COLUMN IF NOT EXISTS).2. Pydantic models
SuperConfig: dropssuper_fee_fraction, grows the two new fields.UpdateSuperConfigDatamirrors withOptional[float]for each + the existing0 ≤ x ≤ 1validator applied to both.Machine/CreateMachineData/UpdateMachineData: gainoperator_cash_in_fee_fractionandoperator_cash_out_fee_fractionwith the same validator.super + operator > cap(cap value TBD per open design question 1 in #37).3.
parse_settlementmath — fix to principal-based, direction-awarebitspire.py:256-257currently:→ should be:
Function signature gains the two direction-specific operator fractions OR a reference to the
Machinerow (cleaner — let the function read the right field by tx_type internally).4. Validate
fee_satsfrom bitspire matches computed total (optional, behind a flag)expected_total_fee_sats = platform_fee_sats + operator_fee_satsIf
abs(extra.get("fee_sats") - expected_total_fee_sats) > tolerance(e.g. 2 sats for rounding), record settlement withstatus='disputed'+error_messagedescribing the mismatch. Operator sees disputed-status settlements in the UI for manual review.Tolerance ±2 sats covers the round-half-up/down divergence between bitspire's principal-based fee computation and satmachineadmin's recompute. Anything outside that range is bitspire-side firmware drift or active manipulation.
This validation only meaningfully works AFTER Layer 2 + Layer 3 ship (because before then, bitspire is sending hardcoded 7.77% that won't match anything the operator configured). Implement behind a config flag (
enforce_fee_match: boolon SuperConfig, defaultfalse) so Layer 1 can ship without breaking pre-Layer-2 settlements.5. Caller-site update
tasks.py:_handle_payment(the only caller ofparse_settlement) passes through both theMachinerow and the inferredtx_typefromPayment.extra.6. UI
Machine detail modal grows two fee inputs (Quasar
<q-input>type="number", min=0, max=1, step=0.001, suffix display showing percentage) labelled "Cash-in fee" and "Cash-out fee". Super-config admin page mirrors with the corresponding pair.Tests
test_parse_settlement_principal_based_math_cash_in— assert cash-in split usescash_infractions against principaltest_parse_settlement_principal_based_math_cash_out— assert cash-out split usescash_outfractions against principaltest_parse_settlement_zero_operator_fee_is_legacy_compatible— both fractions = 0 → super gets their share, operator gets 0; valid edge for a free-charge ATMtest_machine_crud_round_trips_both_operator_fee_fractions— create + update + fetch preserves both valuestest_create_machine_validates_operator_fee_range—0 ≤ x ≤ 1, raises on out-of-range, for both fieldstest_super_plus_operator_cap_validation— single-direction sum > cap raises 400 (TBD once cap is decided)test_fee_sats_mismatch_records_disputed_status(behind enforce flag) — bitspire sends inconsistent fee_sats → settlement.status = 'disputed', error_message names the divergencetest_super_config_migration_backfills_directional_from_singleton— pre-migrationsuper_fee_fraction=0.05→ post-migrationsuper_cash_in_fee_fraction=0.05, super_cash_out_fee_fraction=0.05Migration notes
Existing rows get directional fee fractions = 0 (operator) or backfilled-from-singleton (super). This is a silent change in behavior for existing settlements once the new math fires: super starts getting their full configured fraction of principal (≈13× more than today) for both directions, operator initially gets 0 until they go set their per-machine rates via the UI. Operators should be told to set their rates before this migration deploys.
Per the existing fork-migrations pattern, this is
migrations_fork.pymaterial — keepsmigrations.pybyte-identical to upstream.Future-proofing for promos
Per parent #37's "Future-proofing for promos" section: the principal-based math + per-direction independence + per-component settlement recording all preserve clean extensibility for future per-user discounts. Don't pre-add a
discount_satscolumn now — single ADD COLUMN later when promo work scopes.Out of scope (separate sub-issues)
aiolabs/lamassu-next#57)Parent: #37
Schema update — adding
fee_mismatch_satsper lnbits advisoryPer the lnbits session advisory in
~/dev/coordination/log.md§2026-06-01T07:00Z, adopting the phasedenforce_fee_matchposture:Schema delta (adds one column to the migration in #38)
Settlement-handler shape (phase 1)
Tolerance value is the lnbits draft (max(1, principal_sats × 0.001)) — open to revising once we have data. The phase-2 promotion criterion is "p99 of |fee_mismatch_sats| well under the tolerance over N weeks of phase-1 data."
Promo extensibility note
The discount-extension future-proofing referenced in the §
20:45Zlog entry stays clean here: when promos arrive, the expected_fee_sats formula gains a- discount_satsterm, andfee_mismatch_satsstill answers the same question ("did bitspire's fee report match our model?"). No schema rework — the futurediscount_satscolumn is independent of this one.refs: coord-log §
2026-06-01T07:00Z(lnbits advisory), §2026-06-01T13:30Z(this session adopting)