Skip to content

buyer: mint fees may fall outside the budget cap — total outlay can exceed the ceiling an operator set #185

Description

@orveth

Observed on a live testnut trade. Not filed as a defect — filed as a question the money model should answer explicitly, because the answer determines whether a budget cap means what an operator thinks it means.

What happened

A 5-sat trade, settled end to end, on a home configured per_job_budget_sats = 5 / total_budget_sats = 5:

  • wallet balance before: 10 sats
  • wallet balance after: 4 sats
  • budget ledger (spent.jsonl): 1 entry, amount_sats: 5, attempt 42cfbaf56e46096ca689e23d281f9da8e8ba7d2d9b059e07e7e627bcb65f250b

So the recorded spend was 5, the cap correctly bounded the authorized payment at 5, and the gate did exactly its job — but 6 sats left the wallet. On this trade size that is 20% above the recorded figure.

The question

If mint fees (or swap/keyset costs) sit outside the budget ledger, then total_budget_sats bounds authorized payments, not total outlay. An operator who sets a 100-sat ceiling to mean "this daemon cannot cost me more than 100 sats" would be wrong by whatever the aggregate fee load turns out to be.

Both readings are defensible — a cap on what we commit to sellers is a coherent design — but it should be a decision that is written down, not an artifact nobody checked.

Cause not determined

I did not diagnose it and am not asserting a mechanism. Candidates:

  • a cashu swap / keyset fee on producing the 5-sat payment from a 10-sat proof
  • a proof in flight that balance_sats does not yet count (in which case the 1 sat returns and there is no gap at all)
  • fee accounting that genuinely happens outside crate::budget

The first and third are real; the second would make this a non-issue. Distinguishing them is the point of the reproduction below.

Reproduction shape

Run the same trade at a larger amount, where a fixed fee separates cleanly from a rounding or denomination artifact — e.g. 50 sats from a 100-sat balance. Then compare three numbers rather than two:

  1. wallet balance delta
  2. spent.jsonl total
  3. wallet balance again after any pending proof settles (to rule out the in-flight case)

A fixed 1-sat gap at both 5 and 50 sats points at a per-operation fee; a gap that scales points at a proportional one; a gap that closes on re-read was never real.

Second customer for the answer

The monetization design (seller pays 10% per trade, with the seller forwarding post-redeem) performs a per-trade payment whose fee accounting hits this identical question, at every trade rather than occasionally. Whatever is decided here should be decided with that in mind, since a per-trade forward multiplies any per-operation cost by trade volume.

Provenance

Found while dogfooding the delivery-watcher slice (PR #181) — the trade itself succeeded: delivery to payment in 2 seconds, unassisted, receipt above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions