Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save vincenzopalazzo/5f9a15c2feb8ef69db9974bd022f6f85 to your computer and use it in GitHub Desktop.

Select an option

Save vincenzopalazzo/5f9a15c2feb8ef69db9974bd022f6f85 to your computer and use it in GitHub Desktop.
BOLT 12 Merchant Cashback Vouchers
bLIP: XX
Title: BOLT 12 Merchant Cashback Vouchers
Status: Draft
Author: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Created: 2026-06-07
License: CC0

Abstract

This bLIP defines a protocol for merchants to grant returning customers a cashback voucher (e.g., 10% of the paid amount in "virtual sats") when they pay in bitcoin over Lightning using BOLT 12. The customer is identified across payments using the bLIP 42 contact mechanism (invreq_contact_secret). The voucher balance is held in the merchant's ledger (e.g., the BitVault Pay backend) and is redeemable only at the issuing merchant, as a discount on a future payment. No funds move at accrual time: the cashback is funded by deducting it from the customer's next payment.

This proposal composes with bLIP 42 (payer identification) and bLIP 56 (PoS payment notifications), but only bLIP 42 support is required.

Copyright

This bLIP is licensed under the CC0 license.

Motivation

A merchant (e.g., a wine shop such as MrVino.ch) wants to incentivize bitcoin payments by offering a loyalty discount: pay in bitcoin, get 10% back as a voucher for your next purchase at the same shop. This is attractive for the merchant (lower fees than cards, repeat customers) and for the customer (effective discount), and it gives Lightning wallets a UX comparable to fiat loyalty programs.

Today there is no interoperable way to do this on Lightning:

  • The merchant cannot recognize a returning customer. bLIP 42 solves this with invreq_contact_secret, but it is specified for contacts between wallets, not for merchant loyalty.
  • There is no way for the merchant to communicate an available voucher balance to the customer's wallet, nor for the wallet to choose between accumulating and redeeming it.

This bLIP specifies both, using only existing BOLT 12 message flows (invoice_request / invoice) plus new odd TLV fields, so unaware wallets continue to work unchanged.

Specification

Actors

  • Customer: BOLT 12 wallet supporting bLIP 42 contact fields and the TLVs defined here. Unsupporting wallets can still pay normally; they simply never accrue or redeem vouchers.
  • Merchant: Always-online Lightning node (or service such as BitVault Pay acting for the merchant) that maintains a voucher ledger: a private database mapping a customer identifier to a voucher balance in msat.
  • Point-of-Sale (optional): A bLIP 56 PoS terminal. It is not on the payment path and does not participate in voucher logic; it only displays the order and payment confirmation. When a voucher is redeemed, the discounted amount is what the PoS must expect (see Interaction with bLIP 56).

Deployment model

The expected deployment is that a payment provider (e.g., BitVault Pay) provides the merchant with both the bitcoin PoS terminal implementing bLIP 56 and the voucher ledger, and operates (or hosts) the merchant's Lightning node. The merchant's only integration step is installing the PoS. In this configuration the provider sees both sides — it constructs the per-order offers at the PoS and processes the notifications at the merchant node — so it can also surface the voucher balance and the accumulate/redeem choice directly on the PoS screen, in addition to the customer's wallet. Nothing in this bLIP requires this co-location: the protocol works identically when the PoS, the merchant node, and the ledger are run by independent parties.

Customer Identification

The customer identifier for the voucher ledger is the bLIP 42 invreq_contact_secret included by the customer in the invoice_request.

Requirements:

  • The customer's wallet MUST obtain explicit user consent before including identifying fields, since loyalty tracking is a deliberate privacy trade-off ("join the loyalty program" action in the wallet).
  • The wallet MUST reuse the same contact_secret for all payments to the same merchant offer, derived deterministically as in bLIP 42, so the balance survives wallet restore from seed.
  • The merchant MUST treat the contact_secret as an opaque ledger key. It MUST NOT require invreq_payer_offer or invreq_payer_bip_353_name for voucher accrual (the merchant never needs to pay the customer back).

Feature Bit

A new feature bit is defined for offer_features:

Bit Name Description
TBD cashback_vouchers Merchant supports this voucher protocol

The merchant SHOULD set this (odd) bit in its offers. A wallet seeing this bit knows it can include a contact_secret to participate and that the invoice may carry voucher TLVs.

New TLV Fields

All types are odd (ignorable by unaware implementations) and TBD, to be reserved in bLIP 2.

invoice TLVs (merchant → customer)

Type Name Description
TBD voucher_balance_msat u64. Customer's redeemable balance at this merchant.
TBD voucher_rate u16. Cashback rate for this payment, in basis points (1000 = 10%).
TBD voucher_applied_msat u64. Amount of voucher applied as a discount to this invoice.

invoice_request TLVs (customer → merchant)

Type Name Description
TBD voucher_redeem_msat u64. Amount of voucher balance the customer asks to apply to this payment.

Protocol Flow

Accrual (first and every paid purchase)

  1. The customer scans the merchant's offer (directly or via a bLIP 56 PoS QR) and sends an invoice_request including invreq_contact_secret (bLIP 42).
  2. The merchant responds with an invoice for the full amount. If the contact_secret is known, the invoice includes voucher_balance_msat (the current balance) and voucher_rate. For a new contact_secret, balance is 0.
  3. The customer pays the invoice.
  4. On claiming the payment, the merchant credits the ledger: balance[contact_secret] += paid_amount_msat * voucher_rate / 10_000.

The accrual is computed on the amount actually paid in this payment, net of any voucher discount applied (no cashback on the cashback).

Redemption (subsequent purchase)

The wallet learns the available balance from voucher_balance_msat in the invoice of step 2 and prompts the user: accumulate or redeem.

  • Accumulate: the customer simply pays the invoice. Flow ends as above.
  • Redeem: the customer sends a new invoice_request for the same offer, now including voucher_redeem_msat. The merchant:
    1. Verifies voucher_redeem_msat <= balance[contact_secret].
    2. Issues a new invoice with invoice_amount = order_amount - voucher_applied_msat, where voucher_applied_msat = min(voucher_redeem_msat, order_amount), and includes voucher_applied_msat and the post-redemption voucher_balance_msat.
    3. On payment claim, debits the ledger by voucher_applied_msat and credits the new accrual on the discounted amount paid.

If the redeeming payment is never completed, the ledger MUST NOT be debited: the debit happens only when the discounted invoice is claimed.

Customer                                Merchant (+ ledger)
   |                                         |
   |  1. invoice_request                     |
   |     (invreq_contact_secret) ----------> |  lookup balance
   |                                         |
   |  2. <--------------------- invoice      |
   |        (amount, voucher_balance_msat,   |
   |         voucher_rate)                   |
   |                                         |
   |  -- user chooses: redeem --             |
   |                                         |
   |  3. invoice_request                     |
   |     (invreq_contact_secret,             |
   |      voucher_redeem_msat) ------------> |  check balance
   |                                         |
   |  4. <--------------------- invoice      |
   |        (amount - applied,               |
   |         voucher_applied_msat,           |
   |         voucher_balance_msat)           |
   |                                         |
   |  5. payment --------------------------> |  debit applied,
   |                                         |  credit accrual

The two-round-trip redemption is intentional: the balance is only known after the first invoice, and the user decision must happen before the discounted invoice is issued. Both round trips are onion messages and complete in seconds at a PoS.

Requirements summary

The merchant:

  • MUST key the ledger by invreq_contact_secret per offer.
  • MUST only credit accrual when the payment is claimed (preimage released).
  • MUST only debit redemption when the discounted invoice is claimed.
  • SHOULD include voucher_balance_msat and voucher_rate in every invoice issued to a known contact_secret.
  • MUST ignore voucher_redeem_msat exceeding the balance (or issue an undiscounted invoice), never issue a negative-amount invoice.
  • MAY expire balances; expiry policy is out of protocol scope but SHOULD be disclosed to the customer out of band.

The customer's wallet:

  • MUST NOT include voucher_redeem_msat without an explicit user choice.
  • SHOULD display the balance, the applied discount, and the new balance.
  • MUST treat voucher TLVs as informational from an untrusted party: the ledger is custodial trust in the merchant (see Trust Model).

Interaction with bLIP 56

When the merchant order is presented through a bLIP 56 PoS:

  • The PoS builds the per-order offer for the full order amount as usual.
  • If the customer redeems a voucher, the merchant's payment_notification will carry amount_msat lower than the order amount the PoS expects. To avoid a spurious notification_nack, the merchant SHOULD include the new voucher_applied_msat TLV in the payment_notification, and the PoS MUST accept a payment where amount_msat + voucher_applied_msat >= expected_amount.
  • The PoS SHOULD display the discount on the confirmation screen and record it for accounting (the discount is a merchant rebate, relevant for taxes).

Additional payment_notification TLV (extends bLIP 56)

Type Name Description
TBD voucher_applied_msat u64. Voucher discount applied to this order.

Trust Model

The voucher balance is a custodial IOU from the merchant, comparable to a paper loyalty card. The customer cannot prove the balance and the merchant can refuse redemption. This is acceptable because:

  • The amounts are small (a percentage of past purchases).
  • The merchant's incentive is repeat business; defrauding loyalty customers is self-defeating.
  • No funds are locked: the customer never pre-pays for the voucher; it is funded entirely as a discount on a future payment.

A merchant-signed accrual receipt (e.g., signing contact_secret || balance || timestamp) would let customers prove balances and is listed as a possible extension in Open Questions.

Privacy Considerations

  • Participating links all of a customer's payments at one merchant. This is the entire point of a loyalty program, but it MUST be opt-in per merchant.
  • The contact_secret is scoped per merchant offer (per bLIP 42 derivation), so merchants cannot correlate a customer across different merchants.
  • A customer can leave the program at any time by simply omitting the contact fields (forfeiting the balance).

Security Considerations

Leaked contact_secret: anyone who learns the secret can redeem the victim's balance (they cannot steal funds, only the discount). Wallets MUST treat contact secrets as sensitive data. Merchants MAY additionally require the bLIP 42 invreq_payer_bip_353_signature (or a payer-key signature over the invoice_request) before honoring redemption above a threshold; this is left optional to keep the base flow working for minimal wallets.

Double redemption race: two concurrent redeeming invoice_requests against the same balance MUST NOT result in over-redemption; the merchant debits at claim time and MUST cap total in-flight voucher_applied_msat at the balance.

Amount games at a PoS: as in bLIP 56, the customer could alter the offer amount; the PoS validation rule above (amount + voucher_applied >= expected) keeps post-payment validation intact.

Rationale

Why a merchant-side ledger instead of on-chain/on-Lightning value?

Issuing real sats back per payment (streaming cashback) costs fees and liquidity per purchase and leaks the customer's receiving offer to the merchant. An ecash or token-based voucher would be bearer-redeemable but adds a mint and a whole trust framework. A ledger keyed by the existing bLIP 42 identifier needs zero new cryptography, zero locked funds, and matches how loyalty programs already work. The voucher never leaves the issuing merchant, so a custodial ledger is the natural fit.

Why fund the cashback from the next payment?

No money moves at accrual time, so the merchant carries no payable and the customer takes no custodial deposit risk beyond an expected discount. It also makes the incentive structurally a retention tool: the value only materializes if the customer returns.

Why reuse bLIP 42 instead of a new identifier?

contact_secret already solves deterministic, seed-restorable, per-peer mutual identification, and wallets are starting to implement it. Reusing it means a wallet with bLIP 42 support only needs the two voucher TLVs and a UX prompt to participate.

Why signal the balance in the invoice?

The invoice is the only merchant→customer message in the base BOLT 12 flow that exists before payment. Carrying the balance there lets the wallet make the accumulate/redeem decision with no extra protocol messages and no merchant web API.

Backwards Compatibility

All new TLVs use odd types: wallets and nodes that do not understand them ignore them and pay normally (they just never accrue a balance the merchant advertises... the merchant MAY still accrue for any invoice_request carrying a contact_secret, so a bLIP 42-only wallet accrues silently and discovers the balance once upgraded). bLIP 56 PoS terminals without the voucher_applied_msat extension will nack discounted payments, so merchants SHOULD NOT enable redemption on orders flowing through a non-upgraded PoS.

Reference Implementation

TODO: BitVault Pay backend (merchant ledger + invoice TLVs), the BitVault-provided bLIP 56 PoS terminal, and a wallet prototype. Pilot deployment planned with MrVino.ch.

Open Questions

  1. Idea validation: to be proposed on Delving Bitcoin / lightning-dev for feedback before requesting a bLIP number and bLIP 2 TLV reservations.
  2. Provable balances: should the merchant return a signed accrual receipt so customers can prove their balance (and merchants can be audited)?
  3. Redemption authentication: should redemption above some amount REQUIRE a payer signature instead of bare contact_secret possession?
  4. Voucher expiry: in-protocol expiry signaling (e.g., voucher_expiry TLV) vs out-of-band terms.
  5. Fiat-denominated balances: the ledger is msat-denominated; merchants pricing in fiat (CHF) may prefer fiat-denominated vouchers. A currency TLV mirroring offer_currency could be added.
  6. Regulatory: whether a merchant-scoped, non-transferable discount balance constitutes e-money in target jurisdictions (CH) is out of scope here but must be validated before deployment.

Acknowledgements

Builds on bLIP 42 (Bastien Teinturier) and the bLIP 56 PoS notification design (Vincenzo Palazzo, Jeffrey Czyz).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment