feat: operator-configurable fee architecture (super% + per-machine operator%) — replaces bitspire-hardcoded fee #37

Closed
opened 2026-05-31 20:18:18 +00:00 by padreug · 1 comment
Owner

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 fee (X%) — set by the lnbits administrator in satmachineadmin. Singleton; applies across every machine on the lnbits instance. Calculated against principal. Separate super_cash_in_fee_fraction and super_cash_out_fee_fraction.
  • Operator fee (Y%) — set per-machine by the ATM operator. Sits on top of the super fee. Calculated against principal. Distributed to commission legs as today. Separate operator_cash_in_fee_fraction and operator_cash_out_fee_fraction.
  • Total fee charged to the customer = (X + Y)% of principal, computed independently per flow direction.
  • Distribution: super gets X% of principal; operator gets Y% of principal, which then flows through the operator's existing commission-leg rules.

What's wrong today (2026-05-31)

Bug 1 — super_fee_fraction math is wrong, super is under-paid ~13× per cashout

bitspire.py:256-257 interprets super_fee_fraction as "fraction of the total fee bitspire sent" rather than "fraction of principal":

platform_fee_sats = round(fee_sats * super_fee_fraction)
operator_fee_sats = fee_sats - platform_fee_sats

Concrete demonstration from tonight's settlement azSz9uEcUaiFHZaJ8eqS5S:

Current Per intended spec
principal_sats 222,320 222,320
fee_sats (bitspire's total) 17,274 (irrelevant — satmachineadmin would compute)
super_fee_fraction configured 0.05 0.05
super gets 864 sats (5% of fee) 11,116 sats (5% of principal)

Same 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-291 in aiolabs/lamassu-next:

const cashInFeeFraction = ref(0.0333)   // 3.33% cash-in
const cashOutFeeFraction = ref(0.0777)  // 7.77% cash-out

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_fraction fields exist

Machine, CreateMachineData, UpdateMachineData in models.py carry 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

Layer Issue Repo Scope
1 — DB/API/math fix #38 aiolabs/satmachineadmin Per-machine operator_cash_in_fee_fraction + operator_cash_out_fee_fraction + principal-based split math + optional fee_sats validation. Closes Bug 1 standalone.
2 — Nostr publisher #39 aiolabs/satmachineadmin Publish bitspire-fees:<atm_pubkey> kind-30078 events on machine create / fee edit / super-config change. Partners with aiolabs/lamassu-next#57 — wire-format must be agreed jointly.
3 — Bitspire consumer aiolabs/lamassu-next#57 aiolabs/lamassu-next Subscribe to kind-30078 fee envelope; drop hardcoded cashInFeeFraction / 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

  • Separate cash-in and cash-out fees at every levelSuperConfig and Machine both grow paired fields (*_cash_in_fee_fraction, *_cash_out_fee_fraction). Wire-format carries cash_in_fee_fraction and cash_out_fee_fraction as 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:

  1. Total-fee cap policy — hardcoded ceiling, super-admin-settable, or unrestricted? My straw is ~25% per direction as a sanity ceiling.
  2. enforce_fee_match default — once Layers 2+3 ship, should satmachineadmin reject settlements whose fee_sats doesn't match principal × (super + operator) ± tolerance? Recommend "yes, default-on" once the wire-format reaches the ATM reliably.
  3. Apply-mid-transaction policy — if a fee-config event arrives between QR-scan and pay, do we apply or hold-until-completion? Recommend hold-until-completion.

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:

  1. schema_version in 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.
  2. Principal-based math is the clean substrate — once Layer 1 ships, super_fee + operator_fee are computed independently against principal, so future per-user discounts can subtract from either share cleanly (no "split the discount across both" entanglement to unwind later). A future discount field on the wire payload could carry applies_to: "super" | "operator" | "both".
  3. Payment.extra.nostr_sender_pubkey is already there (from path B) — promos can later key targeting on customer's npub without any new metadata plumbing.
  4. Settlement table already records super_fee_sats + operator_fee_sats independently — future discount_sats is 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.md with 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:

Legacy concept Nostr-native equivalent
Membership ID (QR/NFC scan) Customer's npub (already plumbed via path B)
external_user_id field npub itself — globally unique, no lookup table
Discount tier registry satmachineadmin roster, published via kind-30078 at bitspire-memberships:<atm_pubkey> (mirrors cassette + fee-config patterns)
Lightning Address column Resolved on-the-fly via NIP-05 / kind:10063 zap targets / kind:0 profile metadata — already solved in the Nostr stack
membership_usage audit table Payment.extra.nostr_sender_pubkey on every settlement, free from path B

The 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

  • Today's path-B work that surfaced this: log §19:35Z archived at ~/dev/coordination/archive/2026-05-31-path-b-shipped.md
  • parse_settlement math: bitspire.py:217-263
  • Singleton config: models.py:411-433 (SuperConfig, UpdateSuperConfigData)
  • bitspire-side hardcoded fees: aiolabs/lamassu-next apps/machine/src/stores/atm.ts:290-291
  • Existing operator-config Nostr channel (cassette config, pattern to extend): aiolabs/satmachineadmin#29 (PR #30), aiolabs/lamassu-next#56
  • Legacy membership/discount design: aiolabs/lamassu-server#1 + ~/dev/coordination/membership-discount-plan-legacy-reference.md
## 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 fee (X%)** — set by the lnbits administrator in satmachineadmin. Singleton; applies across every machine on the lnbits instance. Calculated against principal. Separate `super_cash_in_fee_fraction` and `super_cash_out_fee_fraction`. - **Operator fee (Y%)** — set per-machine by the ATM operator. Sits on top of the super fee. Calculated against principal. Distributed to commission legs as today. Separate `operator_cash_in_fee_fraction` and `operator_cash_out_fee_fraction`. - **Total fee charged to the customer = (X + Y)%** of principal, computed independently per flow direction. - **Distribution**: super gets X% of principal; operator gets Y% of principal, which then flows through the operator's existing commission-leg rules. ## What's wrong today (2026-05-31) ### Bug 1 — `super_fee_fraction` math is wrong, super is under-paid ~13× per cashout `bitspire.py:256-257` interprets `super_fee_fraction` as *"fraction of the total fee bitspire sent"* rather than *"fraction of principal"*: ```python platform_fee_sats = round(fee_sats * super_fee_fraction) operator_fee_sats = fee_sats - platform_fee_sats ``` Concrete demonstration from tonight's settlement `azSz9uEcUaiFHZaJ8eqS5S`: | | Current | Per intended spec | |---|---|---| | principal_sats | 222,320 | 222,320 | | fee_sats (bitspire's total) | 17,274 | (irrelevant — satmachineadmin would compute) | | `super_fee_fraction` configured | 0.05 | 0.05 | | super gets | **864 sats** (5% of fee) | **11,116 sats** (5% of principal) | Same 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-291` in `aiolabs/lamassu-next`: ```ts const cashInFeeFraction = ref(0.0333) // 3.33% cash-in const cashOutFeeFraction = ref(0.0777) // 7.77% cash-out ``` 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_fraction` fields exist `Machine`, `CreateMachineData`, `UpdateMachineData` in `models.py` carry 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 | Layer | Issue | Repo | Scope | |---|---|---|---| | 1 — DB/API/math fix | **#38** | `aiolabs/satmachineadmin` | Per-machine `operator_cash_in_fee_fraction` + `operator_cash_out_fee_fraction` + principal-based split math + optional fee_sats validation. **Closes Bug 1 standalone.** | | 2 — Nostr publisher | **#39** | `aiolabs/satmachineadmin` | Publish `bitspire-fees:<atm_pubkey>` kind-30078 events on machine create / fee edit / super-config change. **Partners with `aiolabs/lamassu-next#57`** — wire-format must be agreed jointly. | | 3 — Bitspire consumer | **`aiolabs/lamassu-next#57`** | `aiolabs/lamassu-next` | Subscribe to kind-30078 fee envelope; drop hardcoded `cashInFeeFraction` / `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 - ✅ **Separate cash-in and cash-out fees at every level** — `SuperConfig` and `Machine` both grow paired fields (`*_cash_in_fee_fraction`, `*_cash_out_fee_fraction`). Wire-format carries `cash_in_fee_fraction` and `cash_out_fee_fraction` as 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`: 1. **Total-fee cap policy** — hardcoded ceiling, super-admin-settable, or unrestricted? My straw is ~25% per direction as a sanity ceiling. 2. **`enforce_fee_match` default** — once Layers 2+3 ship, should satmachineadmin reject settlements whose `fee_sats` doesn't match `principal × (super + operator)` ± tolerance? Recommend "yes, default-on" once the wire-format reaches the ATM reliably. 3. **Apply-mid-transaction policy** — if a fee-config event arrives between QR-scan and pay, do we apply or hold-until-completion? Recommend hold-until-completion. ## 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: 1. **`schema_version` in 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. 2. **Principal-based math is the clean substrate** — once Layer 1 ships, super_fee + operator_fee are computed independently against principal, so future per-user discounts can subtract from either share cleanly (no "split the discount across both" entanglement to unwind later). A future discount field on the wire payload could carry `applies_to: "super" | "operator" | "both"`. 3. **`Payment.extra.nostr_sender_pubkey` is already there** (from path B) — promos can later key targeting on customer's npub without any new metadata plumbing. 4. **Settlement table already records super_fee_sats + operator_fee_sats independently** — future `discount_sats` is 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.md`** with 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: | Legacy concept | Nostr-native equivalent | |---|---| | Membership ID (QR/NFC scan) | Customer's npub (already plumbed via path B) | | `external_user_id` field | npub itself — globally unique, no lookup table | | Discount tier registry | satmachineadmin roster, published via kind-30078 at `bitspire-memberships:<atm_pubkey>` (mirrors cassette + fee-config patterns) | | Lightning Address column | Resolved on-the-fly via NIP-05 / `kind:10063` zap targets / `kind:0` profile metadata — already solved in the Nostr stack | | `membership_usage` audit table | `Payment.extra.nostr_sender_pubkey` on every settlement, free from path B | The 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 - Today's path-B work that surfaced this: log §`19:35Z` archived at `~/dev/coordination/archive/2026-05-31-path-b-shipped.md` - `parse_settlement` math: `bitspire.py:217-263` - Singleton config: `models.py:411-433` (`SuperConfig`, `UpdateSuperConfigData`) - bitspire-side hardcoded fees: `aiolabs/lamassu-next` `apps/machine/src/stores/atm.ts:290-291` - Existing operator-config Nostr channel (cassette config, pattern to extend): `aiolabs/satmachineadmin#29` (PR #30), `aiolabs/lamassu-next#56` - Legacy membership/discount design: `aiolabs/lamassu-server#1` + `~/dev/coordination/membership-discount-plan-legacy-reference.md`
Author
Owner

Closing — joint smoke succeeded 2026-06-01

All three layers shipped + validated end-to-end on the Sintra with a physical cash-out.

Status

Layer Issue Status
1 — DB/API/math (sat) #38 merged via PR #42
2 — Nostr publisher (sat) #39 merged via PR #43
3 — Bitspire consumer aiolabs/lamassu-next#57 deployed at 9bdb933 on the Sintra

Both bugs closed

  • Bug 1 (super under-payment ~13×) — Layer 1 principal-based math:
    • Pre-fix: super got 864 sats on a 222,320-sat principal at 5% (5% of fee, not principal)
    • Post-fix smoke: super got 3,421 sats on a 114,030-sat principal at 3% (= 3% of principal, exactly intended)
  • Bug 2 (bitspire hardcodes the total fee) — Layer 2 publisher → Layer 3 consumer:
    • Pre-fix: Sintra hardcoded cashInFeeFraction = 0.0333 / cashOutFeeFraction = 0.0777
    • Post-fix smoke: Sintra received cash_in_fee_fraction = 0.08 / cash_out_fee_fraction = 0.08 via NIP-44-v2-encrypted kind-30078 event, applied reactively without restart

Joint smoke result (settlement T7yoh8BuEeXer79J9X9Swh)

  • Customer paid: 70 EUR cash-out
  • principal_sats: 114,030
  • fee_sats (bitspire-claimed): 9,122 (= 8% of principal)
  • platform_fee_sats (sat-side computed): 3,421 (super 3% of principal → super wallet)
  • operator_fee_sats (sat-side computed): 5,702 (operator 5% of principal → commission legs: 4562 + 1140)
  • fee_mismatch_sats: -1 (rounding drift, well within tolerance of 114; Phase-1 observability working as designed)
  • status: processed

Open design questions — all answered during the work

  1. Total-fee cap policy → locked at 15% per direction, hardcoded, defense-in-depth both producer + consumer (coord-log §07:22Z)
  2. enforce_fee_match default → Phase 1 ship as observability-only via fee_mismatch_sats column; Phase 2 (reject) lands as follow-up once data justifies the threshold (coord-log §07:00Z)
  3. Apply-mid-transaction policy → falls out for free of bitspire's XState context-snapshot at flow start (coord-log §07:22Z)

Wire-format documented decision arc

Worth noting one architectural reversal in the coord-log thread: bitspire originally proposed threading components through 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_settlements is 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_version in wire payload — version-gated upgrade path open
  • Principal-based math — clean substrate for future per-customer discounts (no entanglement to unwind)
  • Payment.extra.nostr_sender_pubkey — promo targeting plumbing already exists from path B
  • Per-component settlement recording (platform_fee_sats + operator_fee_sats) — single ADD COLUMN discount_sats away from promo support

Cleanup follow-ups (none blocking)

  • Gap 1 (aiolabs/lamassu-next): wss://relay.aiolabs.dev hardcoded default points at NXDOMAIN — Padreug policy call on whether to stand the relay up or move to QR-code seeding (#41's territory)
  • Gap 2 (aiolabs/lamassu-next): drop VITE_LNBITS_HTTP_URL + boot echo + manual encodeLnurl composition now that aiolabs/withdraw e9d911e populates link.lnurl upstream
  • #41: republish_operator_configs helper for LocalSigner→RemoteBunkerSigner migration cascade — implementable on top of fee_transport + cassette_transport primitives
  • #38 Phase-2 enforce_fee_match: settlement-reject on out-of-tolerance once data justifies the threshold

Closing the parent.

refs: coord-log §2026-06-01T18:20Z (joint-smoke success entry), PR #42, PR #43, aiolabs/lamassu-next@9bdb933 (Layer 3 deploy), settlement T7yoh8BuEeXer79J9X9Swh

## Closing — joint smoke succeeded 2026-06-01 All three layers shipped + validated end-to-end on the Sintra with a physical cash-out. ### Status | Layer | Issue | Status | |---|---|---| | 1 — DB/API/math (sat) | #38 | ✅ merged via PR #42 | | 2 — Nostr publisher (sat) | #39 | ✅ merged via PR #43 | | 3 — Bitspire consumer | `aiolabs/lamassu-next#57` | ✅ deployed at `9bdb933` on the Sintra | ### Both bugs closed - **Bug 1 (super under-payment ~13×)** — Layer 1 principal-based math: - Pre-fix: super got 864 sats on a 222,320-sat principal at 5% (5% of fee, not principal) - Post-fix smoke: super got 3,421 sats on a 114,030-sat principal at 3% (= 3% of principal, exactly intended) - **Bug 2 (bitspire hardcodes the total fee)** — Layer 2 publisher → Layer 3 consumer: - Pre-fix: Sintra hardcoded `cashInFeeFraction = 0.0333` / `cashOutFeeFraction = 0.0777` - Post-fix smoke: Sintra received `cash_in_fee_fraction = 0.08` / `cash_out_fee_fraction = 0.08` via NIP-44-v2-encrypted kind-30078 event, applied reactively without restart ### Joint smoke result (settlement `T7yoh8BuEeXer79J9X9Swh`) - Customer paid: 70 EUR cash-out - principal_sats: 114,030 - fee_sats (bitspire-claimed): 9,122 (= 8% of principal) - platform_fee_sats (sat-side computed): 3,421 (super 3% of principal → super wallet) - operator_fee_sats (sat-side computed): 5,702 (operator 5% of principal → commission legs: 4562 + 1140) - **fee_mismatch_sats: -1** (rounding drift, well within tolerance of 114; Phase-1 observability working as designed) - status: processed ### Open design questions — all answered during the work 1. **Total-fee cap policy** → locked at 15% per direction, hardcoded, defense-in-depth both producer + consumer (coord-log §`07:22Z`) 2. **`enforce_fee_match` default** → Phase 1 ship as observability-only via `fee_mismatch_sats` column; Phase 2 (reject) lands as follow-up once data justifies the threshold (coord-log §`07:00Z`) 3. **Apply-mid-transaction policy** → falls out for free of bitspire's XState context-snapshot at flow start (coord-log §`07:22Z`) ### Wire-format documented decision arc Worth noting one architectural reversal in the coord-log thread: bitspire originally proposed threading `components` through 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_settlements` is 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_version` in wire payload — version-gated upgrade path open - Principal-based math — clean substrate for future per-customer discounts (no entanglement to unwind) - `Payment.extra.nostr_sender_pubkey` — promo targeting plumbing already exists from path B - Per-component settlement recording (platform_fee_sats + operator_fee_sats) — single `ADD COLUMN discount_sats` away from promo support ### Cleanup follow-ups (none blocking) - **Gap 1** (`aiolabs/lamassu-next`): `wss://relay.aiolabs.dev` hardcoded default points at NXDOMAIN — Padreug policy call on whether to stand the relay up or move to QR-code seeding (`#41`'s territory) - **Gap 2** (`aiolabs/lamassu-next`): drop `VITE_LNBITS_HTTP_URL` + boot echo + manual encodeLnurl composition now that `aiolabs/withdraw e9d911e` populates `link.lnurl` upstream - **#41**: `republish_operator_configs` helper for LocalSigner→RemoteBunkerSigner migration cascade — implementable on top of fee_transport + cassette_transport primitives - **#38 Phase-2 `enforce_fee_match`**: settlement-reject on out-of-tolerance once data justifies the threshold Closing the parent. refs: coord-log §`2026-06-01T18:20Z` (joint-smoke success entry), PR #42, PR #43, `aiolabs/lamassu-next@9bdb933` (Layer 3 deploy), settlement `T7yoh8BuEeXer79J9X9Swh`
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/satmachineadmin#37
No description provided.