Communicating fees

In the LSP model, fees are involved when the user wants to receive a payment, but doesn't have a sufficient receivable amount. This section provides recommendations on how to communicate these fees to a user.

Before receiving a Lightning payment

When the user wants to receive a payment, a setup fee is paid when the resulting invoice would exceed the receivable amount. The setup fee is made up of two parts:

  • A minimum fee
  • A proportional fee based on the amount

Before creating an invoice, the amount the user will want to receive is yet unknown. It is recommended to show the user a message consisting of the following information:

A setup fee of x% with a minimum of y sats will be applied for receiving more than z sats.

You can construct this message as follows:

Rust
let inbound_liquidity_msat = sdk.node_info()?.inbound_liquidity_msats;
let inbound_liquidity_sat = inbound_liquidity_msat / 1000;

let opening_fee_response = sdk
    .open_channel_fee(OpenChannelFeeRequest::default())
    .await?;

let opening_fees = opening_fee_response.fee_params;
let fee_percentage = (opening_fees.proportional * 100) as f64 / 1_000_000_f64;
let min_fee_sat = opening_fees.min_msat / 1_000;

if inbound_liquidity_sat == 0 {
    info!("A setup fee of {fee_percentage}% with a minimum of {min_fee_sat} sats will be applied.");
} else {
    info!("A setup fee of {fee_percentage}% with a minimum of {min_fee_sat} sats will be applied for receiving more than {inbound_liquidity_sat} sats.");
}
Swift
if let nodeInfo = try? sdk.nodeInfo() {
    let inboundLiquiditySat = nodeInfo.inboundLiquidityMsats / 1_000;

    let openingFeeResponse = try? sdk.openChannelFee(req: OpenChannelFeeRequest(amountMsat: nil));

    if let openingFees = openingFeeResponse?.feeParams {
        let feePercentage = Double(openingFees.proportional * 100) / 1_000_000.0;
        let minFeeSat = openingFees.minMsat / 1_000;

        if inboundLiquiditySat == 0 {
            print("A setup fee of \(feePercentage)% with a minimum of \(minFeeSat) sats will be applied.");
        } else {
            print("A setup fee of \(feePercentage)% with a minimum of \(minFeeSat) sats will be applied for receiving more than \(inboundLiquiditySat) sats.");
        }
    }
}
Kotlin
val inboundLiquidityMsat = sdk.nodeInfo()?.inboundLiquidityMsats ?: 0u
val inboundLiquiditySat = inboundLiquidityMsat / 1_000u

val openingFeeResponse = sdk.openChannelFee(OpenChannelFeeRequest(null))

val openingFees = openingFeeResponse.feeParams;
val feePercentage = (openingFees.proportional * 100u) / 1_000_000u
val minFeeSat = openingFees.minMsat / 1_000u;

if (inboundLiquiditySat == 0UL) {
    // Log.v("A setup fee of ${feePercentage}% with a minimum of ${minFeeSat} sats will be applied.");
} else {
    // Log.v("A setup fee of ${feePercentage}% with a minimum of ${minFeeSat} sats will be applied for receiving more than ${inboundLiquiditySat} sats.");
}
React Native
const nodeState = await nodeInfo()
const inboundLiquidityMsat = nodeState.inboundLiquidityMsats
const inboundLiquiditySat = inboundLiquidityMsat != null ? inboundLiquidityMsat / 1_000 : 0

const openChannelFeeResponse = await openChannelFee({})

const openingFees = openChannelFeeResponse.feeParams
const feePercentage = (openingFees.proportional * 100) / 1_000_000
const minFeeSat = openingFees.minMsat / 1_000

if (inboundLiquiditySat === 0) {
  console.log(`A setup fee of ${feePercentage}% with a minimum of ${minFeeSat} sats will be applied.`)
} else {
  console.log(`A setup fee of ${feePercentage}% with a minimum of ${minFeeSat} sats will be applied for receiving more than ${inboundLiquiditySat} sats.`)
}
Dart
NodeState? nodeInfo = await BreezSDK().nodeInfo();
if (nodeInfo != null) {
  int inboundLiquiditySat = nodeInfo.inboundLiquidityMsats ~/ 1000;

  OpenChannelFeeResponse openingFeeResponse = await BreezSDK().openChannelFee(req: OpenChannelFeeRequest());

  OpeningFeeParams openingFees = openingFeeResponse.feeParams;
  double feePercentage = (openingFees.proportional * 100) / 1000000;
  int minFeeSat = openingFees.minMsat ~/ 1000;

  if (inboundLiquiditySat == 0) {
      print("A setup fee of $feePercentage% with a minimum of $minFeeSat sats will be applied.");
  } else {
      print("A setup fee of $feePercentage% with a minimum of $minFeeSat sats will be applied for receiving more than $inboundLiquiditySat sats.");
  }
}
Python
inbound_liquidity_msat = sdk_services.node_info().inbound_liquidity_msats
inbound_liquidity_sat = inbound_liquidity_msat / 1_000

opening_fee_response = sdk_services.open_channel_fee()

opening_fees = opening_fee_response.fee_params
fee_percentage = (opening_fees.proportional * 100)  / 1_000_000
min_fee_sat = opening_fees.min_msat / 1_000

if inbound_liquidity_sat == 0:
    print("A setup fee of ", fee_percentage, "% with a minimum of ", min_fee_sat, " sats will be applied.")
else:
    print(
        "A setup fee of ", fee_percentage, "% with a minimum of ", min_fee_sat, " sats will be applied "
        "for receiving more than ", inbound_liquidity_sat, " sats."
    )
Go
if nodeInfo, err := sdk.NodeInfo(); err == nil {
    var inboundLiquiditySat = nodeInfo.InboundLiquidityMsats / 1_000

    if openingFeeResponse, err := sdk.OpenChannelFee(breez_sdk.OpenChannelFeeRequest{}); err == nil {
        var openingFees = openingFeeResponse.FeeParams
        var feePercentage = (openingFees.Proportional * 100) / 1_000_000.0
        var minFeeSat = openingFees.MinMsat / 1_000

        if inboundLiquiditySat == 0 {
            log.Printf(
                "A setup fee of %v%% with a minimum of %v sats will be applied.",
                feePercentage, minFeeSat)
        } else {
            log.Printf(
                "A setup fee of %v%% with a minimum of %v sats will be applied"+
                    "for receiving more than %v sats.",
                feePercentage, minFeeSat, inboundLiquiditySat)
        }
    }
}
C#
try
{
    var nodeInfo = sdk.NodeInfo();

    var inboundLiquiditySat = nodeInfo?.inboundLiquidityMsats / 1_000;

    var openingFeeResponse = sdk.OpenChannelFee(new OpenChannelFeeRequest(null));
    var openingFees = openingFeeResponse?.feeParams;
    if (openingFees != null)
    {
        var feePercentage = (openingFees.proportional * 100) / 1_000_000.0;
        var minFeeSat = openingFees.minMsat / 1_000;

        if (inboundLiquiditySat == 0)
        {
            Console.WriteLine(
                $"A setup fee of {feePercentage}% with a minimum of {minFeeSat} sats will be applied."
            );
        }
        else
        {
            Console.WriteLine(
                $"A setup fee of {feePercentage}% with a minimum of {minFeeSat} sats will be applied " +
                $"for receiving more than {inboundLiquiditySat} sats."
            );
        }
    }
}
catch (Exception)
{
    // Handle error
}

After creating an invoice

After calling receive_payment, you would typically show the recipient a screen containing a QR code with the invoice that the sender can scan.

This is another place to show the user the opening fees applied to the invoice. At this point the amount the user wants to receive is known, so the message can be more concise:

A setup fee of x sats is applied to this invoice.

The fiat amount can also be included. In case of a mobile app it is recommended to communicate to the user that the app has to be run in the foreground in order to be able to receive the payment.

Developer note

Consider implementing the Notification Plugin when using the Breez SDK in a mobile application. By registering a webhook the application can receive a payment notification to process the payment in the background.

Here is how you can build this message:

Rust
let opening_fee_sat = receive_payment_response
    .opening_fee_msat
    .unwrap_or_default()
    / 1000;
info!("A setup fee of {opening_fee_sat} sats is applied to this invoice.");
Swift
let openingFeeSat = (receivePaymentResponse.openingFeeMsat ?? 0) / 1_000;
print("A setup fee of \(openingFeeSat) sats is applied to this invoice.")
Kotlin
val openingFeeSat = receivePaymentResponse.openingFeeMsat?.let{ it.div(1000UL).toULong() } ?: run { 0UL }
// Log.v("Breez", "A setup fee of ${openingFeeSat} sats is applied to this invoice.")
React Native
const openingFeeMsat = receivePaymentResponse.openingFeeMsat
const openingFeeSat = openingFeeMsat != null ? openingFeeMsat / 1000 : 0
console.log(`A setup fee of ${openingFeeSat} sats is applied to this invoice.`)
Dart
int openingFeeSat = (receivePaymentResponse.openingFeeMsat ?? 0) / 1000 as int;
print("A setup fee of $openingFeeSat sats is applied to this invoice.");
Python
opening_fee_msat = receive_payment_response.opening_fee_msat
opening_fee_sat = opening_fee_msat / 1_000 if opening_fee_msat is not None else 0
print("A setup fee of ", opening_fee_sat, " sats is applied to this invoice.")
Go
var openingFeeSat int
openingFeeMsat := receivePaymentResponse.OpeningFeeMsat
if openingFeeMsat != nil {
    openingFeeSat = int(*openingFeeMsat / 1_000)
}
log.Printf("A setup fee of %v sats is applied to this invoice.", openingFeeSat)
C#
var openingFeeSat = receivePaymentResponse.openingFeeMsat.GetValueOrDefault() / 1000;
Console.WriteLine($"A setup fee of {openingFeeSat} sats is applied to this invoice.");

Receiving an on-chain transaction

For receiving onchain, there is a minimum and a maximum amount the user can receive. The fees are made up of the same components as receiving a lightning payment.

The user gets an onchain address from receive_onchain. There is no way to know ahead of time exactly the amount that will be received on this address, so it is recommended to show the user the receivable boundaries and the fees involved:

Send more than v sats and up to w sats to this address. A setup fee of x% with a minimum of y sats will be applied for sending more than z sats. This address can only be used once.

Below code sample constructs this message.

Rust
let swap_info = sdk
    .receive_onchain(ReceiveOnchainRequest::default())
    .await?;

let min_deposit_sat = swap_info.min_allowed_deposit;
let max_deposit_sat = swap_info.max_allowed_deposit;
let inbound_liquidity_sat = sdk.node_info()?.inbound_liquidity_msats / 1000;

if let Some(swap_opening_fees) = swap_info.channel_opening_fees {
    let fee_percentage = (swap_opening_fees.proportional * 100) as f64 / 1_000_000_f64;
    let min_fee_sat = swap_opening_fees.min_msat / 1_000;

    info!("Send more than {min_deposit_sat} sats and up to {max_deposit_sat} sats to this address. \
        A setup fee of {fee_percentage}% with a minimum of {min_fee_sat} sats will be applied \
        for sending more than {inbound_liquidity_sat} sats. This address can only be used once.");
}
Swift
let swapInfo = try? sdk.receiveOnchain(req: ReceiveOnchainRequest())

let minDepositSat = swapInfo!.minAllowedDeposit;
let maxDepositSat = swapInfo!.maxAllowedDeposit;

let nodeInfo = try? sdk.nodeInfo();
let inboundLiquiditySat = nodeInfo!.inboundLiquidityMsats / 1_000;

if let swapOpeningFees = swapInfo!.channelOpeningFees {
    let feePercentage = Double(swapOpeningFees.proportional * 100) / 1_000_000.0;
    let minFeeSat = swapOpeningFees.minMsat / 1_000;

    print("Send more than \(minDepositSat) sats and up to \(maxDepositSat) sats to this address. A setup fee of \(feePercentage)% with a minimum of \(minFeeSat) sats will be applied for sending more than \(inboundLiquiditySat) sats. This address can only be used once.");
}
Kotlin
val swapInfo = sdk.receiveOnchain(ReceiveOnchainRequest())

val minDepositSat = swapInfo.minAllowedDeposit
val maxDepositSat = swapInfo.maxAllowedDeposit
val inboundLiquiditySat = (sdk.nodeInfo()?.inboundLiquidityMsats ?: 0u) / 1_000u

val swapOpeningFees = swapInfo.channelOpeningFees
if (swapOpeningFees != null) {
    val feePercentage = (swapOpeningFees.proportional * 100u) / 1_000_000u
    val minFeeSat = swapOpeningFees.minMsat / 1_000u;

    // Log.v("Send more than ${minDepositSat} sats and up to ${maxDepositSat} sats to this address. A setup fee of ${feePercentage}% with a minimum of ${minFeeSat} sats will be applied for sending more than ${inboundLiquiditySat} sats. This address can only be used once.");
}
React Native
const nodeState = await nodeInfo()
const swapInfo = await receiveOnchain({})

const minDepositSat = swapInfo.minAllowedDeposit
const maxDepositSat = swapInfo.maxAllowedDeposit
const inboundLiquidityMsat = nodeState?.inboundLiquidityMsats
const inboundLiquiditySat = inboundLiquidityMsat != null ? (inboundLiquidityMsat / 1_000) : 0

const swapOpeningFees = swapInfo.channelOpeningFees
if (swapOpeningFees != null) {
  const feePercentage = (swapOpeningFees.proportional * 100) / 1_000_000
  const minFeeSat = swapOpeningFees.minMsat / 1_000

  console.log(`Send more than ${minDepositSat} sats and up to ${maxDepositSat} sats to this address. A setup fee of ${feePercentage}% with a minimum of ${minFeeSat} sats will be applied for sending more than ${inboundLiquiditySat} sats. This address can only be used once.`)
}
Dart
SwapInfo swapInfo = await BreezSDK().receiveOnchain(req: ReceiveOnchainRequest());

int minDepositSat = swapInfo.minAllowedDeposit;
int maxDepositSat = swapInfo.maxAllowedDeposit;

NodeState? nodeInfo = await BreezSDK().nodeInfo();
if (nodeInfo != null) {
  int inboundLiquiditySat = nodeInfo.inboundLiquidityMsats ~/ 1000;

  OpeningFeeParams? swapOpeningFees = swapInfo.channelOpeningFees;
  if (swapOpeningFees != null) {
    double feePercentage = (swapOpeningFees.proportional * 100) / 1000000;
    int minFeeSat = swapOpeningFees.minMsat ~/ 1000;

    print("Send more than $minDepositSat sats and up to $maxDepositSat sats to this address. A setup fee of $feePercentage% with a minimum of $minFeeSat sats will be applied for sending more than $inboundLiquiditySat sats. This address can only be used once.");
  }
}
Python
swap_info = sdk_services.receive_onchain(breez_sdk.ReceiveOnchainRequest())

min_deposit_sat = swap_info.min_allowed_deposit
max_deposit_sat = swap_info.max_allowed_deposit
inbound_liquidity_sat = sdk_services.node_info().inbound_liquidity_msats / 1_000

swap_opening_fees = swap_info.channel_opening_fees
if swap_opening_fees is not None:
    fee_percentage = (swap_opening_fees.proportional * 100)  / 1_000_000
    min_fee_sat = swap_opening_fees.min_msat / 1_000

    print(
        "Send more than ", min_deposit_sat, " sats and up to ", max_deposit_sat, " sats to this address. "
        "A setup fee of ", fee_percentage, "% with a minimum of ", min_fee_sat, " sats will be applied for "
        "sending more than ", inbound_liquidity_sat, " sats. This address can only be used once."
    )
Go
if swapInfo, err := sdk.ReceiveOnchain(breez_sdk.ReceiveOnchainRequest{}); err != nil {
    var minDepositSat = swapInfo.MinAllowedDeposit
    var maxDepositSat = swapInfo.MaxAllowedDeposit

    if nodeInfo, err := sdk.NodeInfo(); err == nil {
        var inboundLiquiditySat = nodeInfo.InboundLiquidityMsats / 1_000

        var swapOpeningFees = swapInfo.ChannelOpeningFees
        var feePercentage = (swapOpeningFees.Proportional * 100) / 1_000_000.0
        var minFeeSat = swapOpeningFees.MinMsat / 1_000

        log.Printf(
            "Send more than %v sats and up to %v sats to this address. "+
                "A setup fee of %v%% with a minimum of %v sats will be applied "+
                "for sending more than %v sats. This address can only be used once.",
            minDepositSat, maxDepositSat, feePercentage, minFeeSat, inboundLiquiditySat)
    }
}
C#
try
{
    var swapInfo = sdk.ReceiveOnchain(new ReceiveOnchainRequest());

    var minDepositSat = swapInfo?.minAllowedDeposit;
    var maxDepositSat = swapInfo?.maxAllowedDeposit;

    var nodeInfo = sdk.NodeInfo();
    var inboundLiquiditySat = nodeInfo?.inboundLiquidityMsats / 1_000;

    var swapOpeningFees = swapInfo?.channelOpeningFees;
    if (swapOpeningFees != null)
    {
        var feePercentage = (swapOpeningFees.proportional * 100) / 1_000_000.0;
        var minFeeSat = swapOpeningFees.minMsat / 1_000;

        Console.WriteLine(
            $"Send more than {minDepositSat} sats and up to {maxDepositSat} sats to this address. " +
            $"A setup fee of {feePercentage}% with a minimum of {minFeeSat} sats will be applied " +
            $"for sending more than {inboundLiquiditySat} sats. This address can only be used once."
        );
    }
}
catch (Exception)
{
    // Handle error
}