Two separate costs apply when spending proofs:
input_fee_ppk (parts per thousand, per input). Spending N proofs from that keyset costs ceil(N * ppk / 1000), rounded up once per transaction.fee_reserve is the ceiling the mint holds back for routing, not an estimate. When the mint supports NUT-08, unused reserve comes back as change.Most flows need no fee arithmetic at all: pass includeFees: true to send (or .includeFees(true) on the wallet.ops builders) and the wallet inflates the outputs so the receiver nets the requested amount. It solves for the amount, not the proof count: on a fragmented wallet the padded token can carry many small proofs, which the receiver then pays to spend. Pass proofsWeHave or a denominations plan when the shape matters. The helpers below are for when you budget, validate, or plan outputs yourself.
| Question | Helper |
|---|---|
| What will this send cost me, before I commit? | wallet.ops.send(amount, proofs).prepare() (see below) |
| What does it cost to spend these exact proofs? | Wallet.getFeesForProofs(proofs) |
| What would N inputs of a keyset cost? | Wallet.getFeesForKeyset(nInputs, keysetId) |
Receiver must net amount: what does the sender add on top? |
Wallet.getFeesToInclude(amount, opts?) |
| What is the most this proof set can send or melt, after fees? | Wallet.maxSpendableAfterFees(proofs, feeReserve?) |
The number to put in front of a user is the input fee on the proofs the wallet will actually select, and that depends on the selection. prepare() runs it and returns the real figures:
const preview = await wallet.ops.send(150, proofs).includeFees(true).prepare();
// preview.amount - the amount to send
// preview.fees - the input fees to swap your proofs
// Receiver's side: they net the amount, and the token carries their spend fee on top.
// For the default split that padding is what getFeesToInclude prices.
const receiverFee = wallet.getFeesToInclude(preview.amount);
const tokenTotal = Amount.sum((preview.sendOutputs ?? []).map((o) => o.blindedMessage.amount));
tokenTotal.equals(preview.amount.add(receiverFee)); // true
// Your side: the token plus the swap's input fee, priced from the inputs it selected.
// This is what you show to the user:
const totalCost = tokenTotal.add(preview.fees);
const { keep, send } = await wallet.completeSwap(preview);
getFeesToInclude prices the padding from the default split of amount; pass nOutputs for a custom denominations plan. selectProofsToSend(proofs, amount, includeFees) is the selection alone: its send sums to the inputs picked, overspend included, not the amount the receiver nets. Price it with getFeesForProofs(send) if you use it directly.
getFeesToInclude returns the amount includeFees would add: the input fee for spending the planned outputs, including the fee outputs themselves (fee outputs also incur fees, so the wallet iterates until the total is stable). Use it to budget before selecting, or to price a custom denomination plan:
// Price a sender-pays-fees send before committing to it
const fee = wallet.getFeesToInclude(1000); // eg 2 on a 1000 ppk keyset
// wallet.send(1000, proofs, { includeFees: true }) creates outputs totalling 1000 + fee
// Custom denomination sets: pass the planned output count instead of the default split
wallet.getFeesToInclude(1000, { nOutputs: 4 });
getFeesForProofs prices a concrete proof set (eg inputs you are about to melt). getFeesForKeyset prices a count when the proofs do not exist yet (eg outputs a swap will create).
maxSpendableAfterFees works in the opposite direction: given a proof set, the largest amount that remains after input fees and, optionally, a melt quote's fee_reserve. Because fee_reserve shrinks with the amount, iterate until the quote stabilizes:
let target = wallet.maxSpendableAfterFees(proofs); // before any quote: input fees only
let quote;
for (let attempt = 0; attempt < 3; attempt++) {
const invoice = await getInvoiceFor(target); // from the receiving Lightning wallet
quote = await wallet.createMeltQuoteBolt11(invoice);
target = wallet.maxSpendableAfterFees(proofs, quote.fee_reserve);
if (target.greaterThanOrEqual(quote.amount)) break; // proofs cover amount + fees: pay this quote
quote = undefined; // reserve ate into the amount: re-quote smaller
}
if (!quote) throw new Error('Melt-all did not converge');
amount + fee_reserve and includeFees.Amount value object used throughout.