Created
April 11, 2026 21:14
-
-
Save vincenzopalazzo/8a14f9651a04a0edf842272f86343185 to your computer and use it in GitHub Desktop.
Split Payments via BOLT 12
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # bLIP-XXXX: Split Payments via BOLT 12 | |
| ``` | |
| bLIP: XXXX | |
| Title: Split Payments via BOLT 12 Offers | |
| Status: Draft | |
| Author: Vincenzo Palazzo <vincenzopalazzodev@gmail.com> | |
| Created: 2026-04-11 | |
| License: CC0 | |
| ``` | |
| ## Abstract | |
| This bLIP defines a protocol for splitting a payment among multiple | |
| participants using BOLT 12 offers. The payer who settles the full | |
| invoice with a merchant can request reimbursement from one or more | |
| friends by issuing per-participant offers that carry structured | |
| split-context metadata in the experimental TLV range. | |
| Wallets that understand the split TLV fields can present a rich | |
| "split request" experience; wallets that do not simply see a normal | |
| offer for the requested amount. | |
| ## Motivation | |
| Splitting a bill is one of the most common payment interactions | |
| between friends, yet Lightning has no standardized mechanism for it. | |
| Users currently resort to out-of-band coordination — mental math, | |
| chat messages with amounts, and manually created invoices — all of | |
| which are error-prone and provide no auditability. | |
| BOLT 12 offers are the natural primitive for this: they are reusable, | |
| they carry descriptive metadata, and they support onion-message-based | |
| invoice negotiation without requiring the participants to share | |
| node identifiers out of band. By defining a small set of | |
| experimental TLV fields, wallets can automate the entire split | |
| workflow while remaining fully backward compatible with existing | |
| BOLT 12 implementations. | |
| ## Protocol Flow | |
| The protocol involves three roles: | |
| - **Payer** (Alice): pays the merchant in full and initiates the split. | |
| - **Participant** (Bob, and possibly others): owes Alice a share. | |
| - **Merchant**: receives the original payment (standard BOLT 12 flow, unchanged). | |
| ### Step 1 — Alice Pays the Merchant | |
| Alice pays the merchant through the normal BOLT 12 user-pays-merchant | |
| flow (offer → invoice_request → invoice → payment). She obtains a | |
| settled invoice containing `invoice_payment_hash` and | |
| `invoice_amount`. | |
| ### Step 2 — Alice Creates a Split Offer | |
| For each participant, Alice's wallet creates an offer: | |
| - `offer_amount` is set to the participant's share (in msat). | |
| - `offer_description` is a human-readable summary, | |
| e.g. `"Split: dinner at Ristorante Da Mario"`. | |
| - `offer_issuer` identifies Alice (e.g. `alice@example.com`). | |
| - `offer_issuer_id` or `offer_paths` are set as usual so the | |
| participant can reach Alice's node. | |
| - `offer_absolute_expiry` SHOULD be set to a reasonable window | |
| (e.g. 7 days). | |
| In addition, Alice's wallet sets the following experimental TLV | |
| fields in the offer (see [Split TLV Fields](#split-tlv-fields)): | |
| | Type | Name | Purpose | | |
| |---------------|---------------------------------|---------| | |
| | 1000000001 | `split_original_payment_hash` | Links to the settled merchant invoice | | |
| | 1000000003 | `split_total_amount` | Total amount paid to the merchant | | |
| | 1000000005 | `split_num_participants` | Number of people in the split (including Alice) | | |
| | 1000000007 | `split_merchant_description` | The merchant's `offer_description` or `offer_issuer` from the original offer | | |
| ### Step 3 — Alice Sends the Offer to the Participant | |
| Alice transmits the offer to Bob. This can happen via: | |
| 1. **QR code or NFC** — if they are physically co-located. | |
| 2. **Onion message** — if Alice knows a route to Bob's node | |
| (e.g. via a shared channel or Bob's `node_id`). | |
| 3. **Out-of-band** — copy-paste the `lno1...` string in a chat | |
| message, email, etc. | |
| ### Step 4 — Bob Pays | |
| Bob's wallet decodes the offer. If it recognizes the `split_*` | |
| TLV fields, it presents a dedicated split-request UI showing | |
| the original merchant, total amount, number of participants, and | |
| Bob's share. If it does not recognize them (they are odd-typed | |
| and therefore ignorable), it shows a normal offer for the requested | |
| amount — the payment still works. | |
| Bob sends an `invoice_request` to Alice via the standard BOLT 12 | |
| flow. Alice's node returns an `invoice`. Bob pays the invoice. | |
| ### Step 5 — Alice Receives Reimbursement | |
| Alice's wallet matches the incoming payment to the split context | |
| (via `split_original_payment_hash`) and marks Bob's share as | |
| settled. | |
| ``` | |
| ┌────────┐ ┌────────┐ ┌──────────┐ | |
| │ Alice │ │ Bob │ │ Merchant │ | |
| └───┬────┘ └───┬────┘ └────┬─────┘ | |
| │ offer (scan/browse) │ | |
| │──────────────────────────────────────>│ (1) normal | |
| │ invoice_request │ BOLT 12 | |
| │<─────────────────────────────────────│ flow | |
| │ invoice │ | |
| │──────────────────────────────────────>│ | |
| │ payment │ | |
| │──────────────────────────────────────>│ | |
| │ │ | |
| │ split offer (QR / onion / chat) │ | |
| │─────────────────>│ │ | |
| │ invoice_request │ │ (2) split | |
| │<─────────────────│ │ reimbursement | |
| │ invoice │ │ flow | |
| │─────────────────>│ │ | |
| │ payment │ │ | |
| │<─────────────────│ │ | |
| │ │ │ | |
| ``` | |
| ## Split TLV Fields | |
| All fields use the experimental offer TLV range | |
| (1000000000–1999999999) as permitted by BOLT 12. All types are | |
| **odd**, making them optional for readers that do not understand them. | |
| ### `split_original_payment_hash` | |
| 1. type: 1000000001 (`split_original_payment_hash`) | |
| 2. data: | |
| * [`sha256`:`payment_hash`] | |
| The `payment_hash` from the settled merchant invoice | |
| (`invoice_payment_hash`). This allows the participant's wallet to | |
| verify (if it witnessed the original payment) and allows Alice's | |
| wallet to correlate reimbursements. | |
| ### `split_total_amount` | |
| 1. type: 1000000003 (`split_total_amount`) | |
| 2. data: | |
| * [`tu64`:`total_msat`] | |
| The total amount Alice paid to the merchant, in millisatoshis. | |
| Combined with `offer_amount` (the participant's share), the wallet | |
| can display the split ratio to the user. | |
| ### `split_num_participants` | |
| 1. type: 1000000005 (`split_num_participants`) | |
| 2. data: | |
| * [`tu64`:`count`] | |
| The total number of participants in the split, **including** the | |
| payer (Alice). A value of 2 means "just you and me". Wallets | |
| SHOULD display this so Bob knows the split is fair. | |
| ### `split_merchant_description` | |
| 1. type: 1000000007 (`split_merchant_description`) | |
| 2. data: | |
| * [`...*utf8`:`description`] | |
| A copy of the merchant's `offer_description` or `offer_issuer` | |
| from the original offer. This gives the participant context about | |
| *what* was paid for, without requiring them to have seen the | |
| original offer. | |
| ## Requirements | |
| ### The Payer (Split Initiator) | |
| A wallet that initiates a split: | |
| - MUST have successfully settled the merchant invoice before | |
| creating split offers. | |
| - MUST set `offer_amount` to the participant's share in | |
| millisatoshis. | |
| - MUST set `offer_description` to include a human-readable | |
| indication that this is a split request. | |
| - MUST set `split_original_payment_hash` to the `payment_hash` | |
| of the settled merchant invoice. | |
| - MUST set `split_total_amount` to the `invoice_amount` from the | |
| settled merchant invoice. | |
| - MUST set `split_num_participants` to the total number of people | |
| sharing the payment (including the payer). | |
| - SHOULD set `split_merchant_description` to the merchant's | |
| `offer_description` or `offer_issuer`. | |
| - SHOULD set `offer_absolute_expiry` to a reasonable deadline. | |
| - SHOULD track the settlement status of each participant's | |
| reimbursement. | |
| ### The Participant (Split Responder) | |
| A wallet that receives a split offer: | |
| - if it recognizes the `split_*` TLV fields: | |
| - SHOULD display the split context (merchant, total, number of | |
| participants, and the user's share) before confirming payment. | |
| - MAY reject the split if the share amount seems unreasonable | |
| relative to the total (e.g. share > total). | |
| - if it does not recognize the `split_*` TLV fields: | |
| - MUST treat it as a normal offer (per the odd-type-is-ok rule). | |
| - MUST follow the standard BOLT 12 invoice_request flow to pay. | |
| ## Backward Compatibility | |
| This proposal is fully backward compatible. All new TLV fields are | |
| in the BOLT 12 experimental range and use odd type numbers, so | |
| existing implementations will ignore them. The split offer is a | |
| valid BOLT 12 offer in all respects; the `split_*` fields are purely | |
| additive UX hints. | |
| A participant using a wallet without split support will simply see | |
| an offer from Alice for a given amount, with a description indicating | |
| it is a split. The payment flow is identical. | |
| ## Rationale | |
| **Why offers and not invoice_requests?** | |
| The "user-pays-merchant" offer flow is the right direction here: | |
| Alice is requesting money *from* Bob. She publishes an offer (she is | |
| the "merchant" in BOLT 12 terms), and Bob requests an invoice and | |
| pays. This reuses the most well-supported BOLT 12 flow and requires | |
| no new message types. | |
| **Why not use `offer_metadata` for the split context?** | |
| `offer_metadata` is opaque and implementation-specific. Defining | |
| named TLV fields in the experimental range is more interoperable: | |
| different wallet implementations can independently parse and display | |
| the split context without needing to agree on an `offer_metadata` | |
| encoding. | |
| **Why include the original `payment_hash`?** | |
| It serves two purposes: (1) Alice's wallet can correlate incoming | |
| reimbursements to the original purchase, and (2) if Bob also has | |
| visibility of the original payment (e.g. they were both present when | |
| the merchant showed the QR code), his wallet can verify the claim. | |
| **Why odd TLV types?** | |
| Odd types are "it's OK to be odd" — readers that don't understand | |
| them simply skip them. This ensures the split offer degrades | |
| gracefully to a plain offer on non-supporting wallets. | |
| **Why not a single multi-party payment to the merchant?** | |
| Coordinating multiple payers to a single invoice introduces | |
| complexity (partial payments, timeouts, atomicity). The | |
| pay-then-reimburse model is simpler, works today with no merchant | |
| cooperation, and matches user expectations from traditional | |
| payment apps. | |
| ## Open Questions | |
| 1. **Privacy of the original payment_hash**: Including the merchant's | |
| `payment_hash` leaks information about Alice's payment to the | |
| participant. Is this acceptable, or should it be optional / | |
| blinded? | |
| 2. **Unequal splits**: The current design supports arbitrary per-participant | |
| amounts (each offer has its own `offer_amount`). Should there be | |
| a TLV field for the split *ratio* or *index* to help wallets | |
| present a unified view? | |
| 3. **Group coordination**: For splits among > 2 people, should there be | |
| a `split_group_id` (random identifier) so wallets that receive | |
| multiple split offers from Alice can group them in the UI? | |
| 4. **Onion message delivery**: Sending split offers via onion messages | |
| is attractive for privacy, but requires Alice to know a route to | |
| each participant. Should this bLIP recommend a specific delivery | |
| mechanism, or remain transport-agnostic? | |
| ## Reference Implementation | |
| *TODO: link to reference implementation once available.* | |
| ## Copyright | |
| This bLIP is licensed under CC0. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment