feat: Layer 3 — consume operator fee config from Nostr; drop hardcoded cashInFeeFraction / cashOutFeeFraction #57
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?
aiolabs/satmachineadmin#37(architectural intent + bug surfaced 2026-05-31)aiolabs/satmachineadmin#39(sat-side: publishes the kind-30078 events this issue consumes)Locked design decisions (per
aiolabs/satmachineadmin#37)schema_versionin payload — version-gated upgrade path for future fields.Why this exists
apps/machine/src/stores/atm.ts:290-291currently hardcodes the fees:Every Sintra that runs the current bitspire build charges those rates regardless of who owns it. The operator can't configure their own fee without rebuilding the firmware. Per the architectural intent in
aiolabs/satmachineadmin#37, the fee should be the sum of:super_cash_*_fee_fraction(X%, instance-wide, per-direction)operator_cash_*_fee_fraction(Y%, set by the ATM operator, per-direction)Both calculated against the principal amount. Total fee per direction = (X+Y)%. The split (super getting X%, operator getting Y%) happens on the satmachineadmin side at settlement time (Layer 1 /
aiolabs/satmachineadmin#38); bitspire's job here is to apply the total (X+Y)% to the principal when generating the invoice for the corresponding flow direction.What ships (consumer side)
Subscribe to the kind-30078 fee-config envelope
Mirrors the cassette-config consumer pattern from
aiolabs/lamassu-next#56— same kind, same encryption envelope. Operator-config-over-Nostr is now a multi-document pattern; this issue adds the second document type on the bitspire side.aiolabs/satmachineadmin#39):30078#p:[atm_pubkey_hex]#d:["bitspire-fees:" + atm_pubkey_hex]lastAppliedPublishedAt(similar tometa.lastKnownConfigCreatedAtfor operator config in#56); reject events older than the last applied.schema_versionhandling: v1 consumer parsescash_in_fee_fraction+cash_out_fee_fraction+published_at; ignores any other top-level keys (future-proofing — sat-side may publish v2 events with promo fields once that work scopes; v1 bitspire should keep working against v2 events with the fee fields it knows).Apply received config
cashInFeeFraction/cashOutFeeFractionrefs to the values from the most recently received event.#56) so a restart restores it without waiting for a fresh publish.[Fees] applied cash_in=X cash_out=Y schema=1 published_at=N.Fail-closed on no-config-received
roster_required=true— the safety net is fail-closed when the prerequisite isn't met.Drop the hardcoded constants
atm.ts:290-291becomes:Both refs start at 0; reactivity propagates the fee changes through the existing CashInView / CashOutView UI without changes there.
Still-open design questions (need joint resolution — mirrors
aiolabs/satmachineadmin#39)Future-proofing for promos
Treat any unknown top-level keys in the wire payload (including a future
discountsarray) as ignorable in this v1 consumer. When a future bitspire ships v2 promo awareness, theschema_versionbump tells it to parse the new fields. Current implementation must not crash on unknown fields. See parentaiolabs/satmachineadmin#37for the broader future-proofing rationale.Tests
test_subscribe_filter_shape— filter exactly matches the wire-format agreementtest_apply_received_fee_config_both_directions— kind-30078 event with both fields populated updates both refs + persiststest_watermark_rejects_older_event— event withpublished_at < lastAppliedPublishedAtis ignoredtest_decryption_failure_logs_and_skips— corrupt ciphertext doesn't crash the consumertest_unknown_top_level_fields_ignored— payload with extra keys (simulated v2 forward-compat) parses successfully, applies the known fieldstest_boot_no_config_no_persistence_shows_maintenance— first boot with empty store + no inbound events → maintenance screentest_boot_persisted_config_proceeds_offline— persisted config but relay unreachable → ATM operates with persisted valuestest_total_fee_cap_rejection_per_direction— config event with either direction sum > cap is dropped (TBD once cap is decided)Sequencing
aiolabs/satmachineadmin#39/ this issue jointly before either side shipsaiolabs/satmachineadmin#38(Layer 1) ships independently — fixes the super under-payment immediatelyaiolabs/satmachineadmin#39+ this issue ship together — must be agreed on the wire shape, then can ship roughly in parallelCross-refs
aiolabs/satmachineadmin#37aiolabs/satmachineadmin#39aiolabs/satmachineadmin#38aiolabs/lamassu-next#56(cassette config consumer)~/dev/coordination/archive/2026-05-31-path-b-shipped.md