@using BTCPayServer.Abstractions.Contracts @using BTCPayServer.Abstractions.Models @using BTCPayServer.Client.Models @using BTCPayServer.Payments @using BTCPayServer.PayoutProcessors @using Microsoft.AspNetCore.Http @using Microsoft.AspNetCore.Routing @using LightningAddressData = BTCPayServer.Data.LightningAddressData @inject IPluginHookService PluginHookService @inject LightningAddressService LightningAddressService @inject PayoutProcessorService PayoutProcessorService @inject IEnumerable PayoutProcessorFactories @inject SatBreaker SatBreaker @inject LinkGenerator LinkGenerator @inject IHttpContextAccessor httpContextAccessor @if (Loading) { } else { @if (NoPayoutProcessors) { } @if (Users?.Any() is not true) { } @foreach (var user in Users) { }

Prism

The prism plugin allows automated value splits for your lightning payments. You can set up multiple prisms, each with their own source (which is a lightning address username, or use * as a catch-all for all invoices settled through Lightning, excluding ones which Prism can handle explicitly) and destinations (which are other lightning addresses or lnurls). The plugin will automatically credit the configured percentage of the payment to the destination (while also making sure there is 2% reserved to cater for fees, don't worry, once the lightning node tells us the exact fee amount, we credit/debit the balance after the payment), and once the configured threshold is reached, a payout will be created. Then, a payout processor will run at intervals and process the payout.

How many sats do you want to accumulate per destination before sending?
When a payout is being generated, how many of its amount in percentage should be excluded to cover the fee? Once the payment is settled, if the lightning node provides the exact fee, the balance is adjusted accordingly.
@foreach (var item in Settings.Splits) { }
@if (StatusMessageModel != null) {
@StatusMessageModel.Message
}
} @code { public bool Loading { get; set; } = true; public List Users { get; set; } public PaymentMethodId pmi { get; set; } = new("BTC", LightningPaymentType.Instance); public bool NoPayoutProcessors { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { PayoutProcessorLink = LinkGenerator.GetUriByAction(httpContextAccessor.HttpContext, "ConfigureStorePayoutProcessors", "UIPayoutProcessors", new {StoreId}); LNAddressLink = LinkGenerator.GetUriByAction(httpContextAccessor.HttpContext, "EditLightningAddress", "UILNURL", new {StoreId}); PayoutsLink = LinkGenerator.GetUriByAction(httpContextAccessor.HttpContext, "Payouts", "UIStorePullPayments", new {StoreId, payoutState = PayoutState.AwaitingPayment, paymentMethodId = pmi.ToString()}); var fetchSettings = SatBreaker.Get(StoreId); var fetchLnAddresses = LightningAddressService.Get(new LightningAddressQuery() { StoreIds = new[] {StoreId} }); var fetchProcessors = PayoutProcessorService.GetProcessors(new PayoutProcessorService.PayoutProcessorQuery() { Stores = new[] {StoreId}, PaymentMethods = new[] {pmi.ToString()} }); var tasks = new Task[] { fetchSettings, fetchLnAddresses, fetchProcessors }; await Task.WhenAll(tasks); Settings = await fetchSettings; Users = await fetchLnAddresses; EditContext = new EditContext(Settings); MessageStore = new ValidationMessageStore(EditContext); EditContext.OnValidationRequested += Validate; EditContext.OnFieldChanged += FieldChanged; SatBreaker.PrismUpdated += SatBreakerOnPrismUpdated; NoPayoutProcessors = PayoutProcessorFactories.Any(factory => factory.GetSupportedPaymentMethods().Contains(pmi)) && !(await fetchProcessors).Any(); Loading = false; await InvokeAsync(StateHasChanged); } await base.OnAfterRenderAsync(firstRender); } private void FieldChanged(object sender, FieldChangedEventArgs e) { StatusMessageModel = null; } private void SatBreakerOnPrismUpdated(object sender, PrismPaymentDetectedEventArgs e) { if (e.Settings != Settings && e.Settings.Version != Settings.Version) { Settings.DestinationBalance = e.Settings.DestinationBalance; Settings.PendingPayouts = e.Settings.PendingPayouts; Settings.Version = e.Settings.Version; } InvokeAsync(StateHasChanged); } private void Validate(object sender, ValidationRequestedEventArgs validationRequestedEventArgs) { var previousState = EditContext.GetValidationMessages().Any(); MessageStore.Clear(); StatusMessageModel = null; foreach (var prism in Settings.Splits) { if (string.IsNullOrEmpty(prism.Source)) { MessageStore.Add(() => prism.Source, "Source is required"); } else if (Settings.Splits.Count(s => s.Source == prism.Source) > 1) { MessageStore.Add(() => prism.Source, "Sources must be unique"); } if (!(prism.Destinations?.Count > 0)) { MessageStore.Add(() => prism.Destinations, "At least one destination is required"); continue; } var sum = prism.Destinations.Sum(d => d.Percentage); if (sum > 100) { MessageStore.Add(() => prism.Destinations, "Destinations must sum up to a 100 maximum"); } foreach (var destination in prism.Destinations) { var dest = destination.Destination; //check that the source is a valid internet identifier, which is username@domain(and optional port) if (string.IsNullOrEmpty(dest)) { MessageStore.Add(() => destination.Destination, "Destination is required"); continue; } if (!ValidateDestination(dest)) { MessageStore.Add(() => destination.Destination, "Destination is not valid"); } } } if (previousState != EditContext.GetValidationMessages().Any()) EditContext.NotifyValidationStateChanged(); } private bool ValidateDestination(string dest) { try { LNURL.LNURL.ExtractUriFromInternetIdentifier(dest); return true; } catch (Exception e) { try { LNURL.LNURL.Parse(dest, out var tag); return true; } catch (Exception exception) { var result = PluginHookService.ApplyFilter("prism-destination-validate", dest).Result; if (result is true) return true; } } return false; } public ValidationMessageStore MessageStore { get; set; } public EditContext EditContext { get; set; } public StatusMessageModel StatusMessageModel { get; set; } public PrismSettings Settings { get; set; } public string PayoutProcessorLink { get; set; } public string LNAddressLink { get; set; } public string PayoutsLink { get; set; } [Parameter] public string StoreId { get; set; } private async Task CreateNewPrism() { Settings.Splits.Add(new Split()); await InvokeAsync(StateHasChanged); } private async Task RemovePrism(Split item) { if (Settings.Splits.Remove(item)) { await InvokeAsync(StateHasChanged); } } private async Task Save() { var settz = await SatBreaker.Get(StoreId); settz.Splits = Settings.Splits; settz.Destinations = Settings.Destinations; settz.Enabled = Settings.Enabled; settz.SatThreshold = Settings.SatThreshold; settz.Reserve = Settings.Reserve; Settings = settz; var updateResult = await SatBreaker.UpdatePrismSettingsForStore(StoreId, settz); if (!updateResult) { StatusMessageModel = new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Error, Message = "The settings have been updated by another process. Please refresh the page and try again." }; } else { StatusMessageModel = new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Success, Message = "Successfully saved settings" }; } } }