-
Notifications
You must be signed in to change notification settings - Fork 77
Proposal: “Capped Input Fee” Melt Quotes #335
Description
This would be an optional extension to Cashu melt quotes that lets a mint promise a capped input fee for a melt, while also bounding the maximum number of inputs that qualify for that promise.
The aim is simple, make PoS and “pay invoice” style flows more reliable, including multi mint routing, without removing or replacing the existing NUT-02 per input fee model.
If this proposal is well received, am happy to raise a PR.
Motivation
Melt fees are currently hard to quote because the input fee depends on how many proofs the wallet ends up sending, and the mint only learns that at melt time.
So while it indicates a fee_reserve for external (lightning) fees, it cannot quote input fees, leaving it to the wallet to calculate.
This gets awkward in flows where:
- a merchant PoS wants to display a single fixed amount to pay before seeing a token from the customer
- It needs confidence the melt will not fail for insufficient fees
- a legitimate customer may be spending a fragmented proof set
Essentially, it's a chicken-egg problem. A PoS using melt needs an amount to ask the customer for, but doesn't know the amount it needs to ask for until it sees the token from the customer.
The UX for failed PoS melts is bad enough, but there is also a privacy concern: failed melts leak information about wallet fragmentation and proof selection to the mint.
A bounded cap makes the melt quote concrete, not just informative.
Goal
Provide an optional, backwards compatible contract that a mint can attach to a melt quote:
- if the wallet spends
max_inputs_capproofs or fewer, the mint input fee is capped atmint_fee_cap - if the wallet spends more than
max_inputs_capproofs, normal NUT-02 fee rules apply
So wallets get an upper bound they can rely on, but mints keep the usual per input fee model outside the bound.
Approach
Add two optional fields to melt quote responses (NUT-05, NUT-23, NUT-25):
-
mint_fee_cap
The maximum NUT-02 input fee the mint will charge for this melt quote, measured in the quote’s unit. This is mint's input fees only, not Lightning routing fees. -
max_inputs_cap
The maximum number of proofs eligible for the cap.
These fields are intended to be used together. If a wallet sees only one of them, it should ignore both and fall back to existing behaviour.
Fee evaluation in the melt endpoint becomes:
- If
len(inputs) <= max_inputs_cap, the mint computes the normal NUT-02 input fee for the provided proofs and chargesmin(normal_fee, mint_fee_cap) - If
len(inputs) > max_inputs_cap, the mint charges the normal NUT-02 input fee, no cap
Wallet funding guidance when cap fields are present:
- Fund the melt with inputs totalling at least
amount + fee_reserve + mint_fee_cap - Include
outputsif you want change returned via existing NUT-08 behaviour, this covers unused Lightningfee_reserveand any unused portion of themint_fee_cap
When the cap fields are absent, existing behaviour remains unchanged.
Implementation can be as simple as computing the normal NUT-02 fee, then applying the conditional cap at melt time.
Cap value calculation
Mints can choose any cap values they like. Here's one simple approach.
Let:
S = amount + fee_reservemin_inputs(S)be the minimum number of proofs needed to representSusing the mint’s supported denominations for that unitdenom_count(S)be the number of supported denominations<= Smax_ppk(unit)be the maximuminput_fee_ppkacross keysets of that unit the mint will accept
Suggested max_inputs_cap
max_inputs_cap = min_inputs(S) + denom_count(S) (optionally clamped by mint policy)
Example: S = 1025 in standard binary denominations
min_inputs(S) = 2(1024, 1)denom_count(S) = 11(1,2,4,8,16,32,64,128,256,512,1024)max_inputs_cap = 13
This avoids an overly tight cap for power of two amounts, where min_inputs is 1, while still bounding worst case work with a slack term that grows slowly.
Suggested mint_fee_cap
mint_fee_cap = (min_inputs(S) * max_ppk(unit) + 999) // 1000
Example: S = 1025 in standard binary denominations
min_inputs(S) = 2max_ppk(unit) = 250mint_fee_cap = 1 sat(2 * 250 = 500, rounded up)
This keeps the cap independent of the specific keyset mix the wallet spends, which is the point of quoting.
Backwards compatibility
- Wallets that do not understand the cap fields can ignore them and continue using existing behaviour.
- Mints can implement this without changing core fee calculation logic, it is a conditional cap applied at melt execution time.
- Change handling reuses existing NUT-08 behaviour, no new mechanism is introduced.
Worked example (with change via NUT-08)
Assume:
- unit = sat
- quote amount = 1000
- Lightning fee reserve (from quote) = 5
- mint returns
mint_fee_cap = 1,max_inputs_cap = 12
Wallet behaviour:
- PoS requests a melt quote and receives:
{
"quote": "Q123…",
"amount": 1000,
"unit": "sat",
"fee_reserve": 5,
"mint_fee_cap": 1,
"max_inputs_cap": 12,
"state": "UNPAID",
"expiry": 1701704757
}- PoS requests customer sends the worst case mint fee under the cap:
- target = 1000 + 5 + 1 = 1006 sats
Customer wallet selects proofs totalling 1006 sats, using 10 proofs (<= 12) and provides outputs for change.
PoS obtains the token from the customer and processes it with mint.
- Mint processes the melt:
len(inputs) = 10 <= max_inputs_cap, so cap applies- normal NUT-02 input fee for those proofs is computed, suppose it is 2 sats
- fee charged =
min(2, 1) = 1 sat
Suppose the Lightning payment uses only 3 sats of the 5 sat fee reserve. Then:
- unused Lightning reserve (2 sats) is returned as change via NUT-08 change signatures
- if the normal input fee had been 0 sats, the unused portion of the 1 sat cap could also be returned as change, provided
outputswere supplied
If the wallet had instead provided 20 proofs (> 12), the cap would not apply and the mint would charge the normal NUT-02 input fee for 20 inputs.
Anticipated Questions
Why a cap vs a percentage “exit fee”?
A percentage fee makes quoting simple but changes melt economics, especially for large payments. It also does not remove the need to bound inputs, as without an input cap you still subsidise pathological fragmentation. This proposal keeps the existing ppk model and adds an optional capped path where quoting matters.
Why not simply over reserve worst case fees and return the difference as change?
Over reserving works, but it can noticeably inflate quoted totals, especially for small payments. That is rough in PoS UX and it increases change handling and dust risk. A cap can offer a tighter, explicit maximum without always pushing totals upwards.
Isn’t the fee cap a DoS vector, since inputs become “free” up to the cap?
That’s why max_inputs_cap exists. The cap applies only up to the stated input bound. Above it, normal NUT-02 fees apply. Mints can also clamp the suggested values to policy limits.
Why does max_inputs_cap need to scale with amount?
If you tie max_inputs_cap too closely to “optimal inputs”, you get silly behaviour for power of two amounts where optimal is 1, benign fragmentation gets rejected. A slack term that grows slowly with amount gives room for normal wallet fragmentation without opening the door to "wheelbarrow" proof sets.
How does this interact with multiple keysets and different input_fee_ppk values?
The cap is a quote promise. Wallets do not need to compute it or reason about keysets at quote time. Mints should compute caps conservatively and keep them valid until quote expiry, even if keysets rotate.
Why not require the wallet to commit to its proof set at quote time?
Proof set commitments add complexity and can reduce privacy by revealing wallet state earlier. It is also not practical in PoS flows. This proposal keeps the quote request simple and moves the “guarantee” into a mint issued upper bound instead.
How should wallets handle change and overpayment?
If a wallet wants to reclaim overpayment, it should include outputs in the melt request (existing NUT-08 behaviour). Overpayment can happen when Lightning routing uses less than fee_reserve, or when the actual input fee is below mint_fee_cap.
What if keysets rotate during the quote lifetime?
The quote promise is authoritative for the quote lifetime. If a mint offers a cap, it should ensure the cap remains valid until expiry.
Why not solve this entirely in wallets with better proof selection?
Wallets can improve selection, but PoS flows start outside the customer wallet, and will benefit from a protocol level upper bound that does not depend on how fragmented the customer’s wallet is, especially in multi mint chains. The cap reduces failures and retries without leaning on wallet heuristics.
Why add new fields instead of reusing fee_reserve?
fee_reserve represents uncertainty in external payment costs (Lightning routing). Bundling mint fee caps into it blurs semantics and complicates integrations. Separate fields keep meaning clear.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status