Skip to content

NUT-25: BOLT12

optional

depends on: NUT-04 NUT-05 NUT-20


This document describes minting and melting ecash with the bolt12 payment method, which uses Lightning Network offers. It is an extension of NUT-04 and NUT-05 which cover the protocol steps of minting and melting ecash shared by any supported payment method.

Mint Quote

For the bolt12 method, the wallet includes the following specific PostMintQuoteBolt12Request data:

{
  "amount": <int|null>,
  "unit": <str_enum[UNIT]>,
  "description": <str|null>,
  "pubkey": <str>
}

Note: While a pubkey is optional as per NUT-20 for NUT-04 it is required in this NUT and the mint MUST NOT issue a mint quote if one is not included.

Privacy: To prevent linking multiple mint quotes together, wallets SHOULD generate a unique public key for each mint quote request.

The mint responds with a PostMintQuoteBolt12Response:

{
  "quote": <str>,
  "request": <str>,
  "amount": <int|null>,
  "unit": <str_enum[UNIT]>,
  "expiry": <int|null>,
  "pubkey": <str>,
  "amount_paid": <int>,
  "amount_issued": <int>
}

Where:

  • quote is the quote ID
  • request is the bolt12 offer
  • expiry is the Unix timestamp until which the mint quote is valid
  • amount_paid is the amount that has been paid to the mint via the bolt12 offer
  • amount_issued is the amount of ecash that has been issued for the given mint quote

Example

Request with curl:

curl -X POST http://localhost:3338/v1/mint/quote/bolt12 -d \
'{"amount": 10, "unit": "sat", "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac"}' \
-H "Content-Type: application/json"

Response:

{
  "quote": "DSGLX9kevM...",
  "request": "lno1qcp...",
  "amount": 10,
  "unit": "sat",
  "expiry": 1701704757,
  "pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
  "amount_paid": 0,
  "amount_issued": 0
}

Check quote state:

curl -X GET http://localhost:3338/v1/mint/quote/bolt12/DSGLX9kevM...

Minting tokens:

curl -X POST https://mint.host:3338/v1/mint/bolt12 -H "Content-Type: application/json" -d \
'{
  "quote": "DSGLX9kevM...",
  "outputs": [
    {
      "amount": 8,
      "id": "009a1f293253e41e",
      "B_": "035015e6d7ade60ba8426cefaf1832bbd27257636e44a76b922d78e79b47cb689d"
    },
    {
      "amount": 2,
      "id": "009a1f293253e41e",
      "B_": "0288d7649652d0a83fc9c966c969fb217f15904431e61a44b14999fabc1b5d9ac6"
    }
  ]
}'

Response:

{
  "signatures": [
    {
      "id": "009a1f293253e41e",
      "amount": 2,
      "C_": "0224f1c4c564230ad3d96c5033efdc425582397a5a7691d600202732edc6d4b1ec"
    },
    {
      "id": "009a1f293253e41e",
      "amount": 8,
      "C_": "0277d1de806ed177007e5b94a8139343b6382e472c752a74e99949d511f7194f6c"
    }
  ]
}

Multiple Issuances

Unlike BOLT11 invoices, BOLT12 offers can be paid multiple times, allowing the wallet to mint multiple times for one quote. The wallet can call the check bolt12 endpoint, where the mint will return the PostMintQuoteBolt12Response including amount_paid and amount_issued. The difference between these values represents how much the wallet can mint by calling the mint endpoint. Wallets MAY mint any amount up to this available difference; in particular, they can mint less than the amount mintable. Mints MUST accept mint requests whose total output amount is less than or equal to (amount_paid - amount_issued).

Mint Settings

A description option SHOULD be set to indicate whether the bolt12 payment method backend supports providing an offer description.

Example MintMethodSetting

{
  "method": "bolt12",
  "unit": <str>,
  "min_amount": <int|null>,
  "max_amount": <int|null>,
  "options": {
    "description": true
  }
}

Melt Quote

For the bolt12 method, the wallet includes the following specific PostMeltQuoteBolt12Request data:

{
  "request": <str>,
  "unit": <str_enum[UNIT]>,
  "options": { // Optional
    "amountless": {
      "amount_msat": <int>
    }
  }
}

Here, request is the bolt12 Offer to be paid and unit is the unit the wallet would like to pay with. For amount-less offers, the options.amountless.amount_msat field can be used to specify the amount in millisatoshis to pay to the offer. If options.amountless.amount_msat is defined and the offer has an amount, they MUST be equal.

The mint responds with a PostMeltQuoteBolt12Response:

{
  "quote": <str>,
  "request": <str>,
  "amount": <int>,
  "unit": <str_enum[UNIT]>,
  "fee_reserve": <int>,
  "state": <str_enum[STATE]>,
  "expiry": <int>,
  "payment_preimage": <str|null>
}

Where fee_reserve is the additional fee reserve required for the Lightning payment. The mint expects the wallet to include Proofs of at least total_amount = amount + fee_reserve + fee where fee is calculated from the keyset's input_fee_ppk as described in NUT-02.

state is an enum string field with possible values "UNPAID", "PENDING", "PAID":

  • "UNPAID" means that the request has not been paid yet.
  • "PENDING" means that the request is currently being paid.
  • "PAID" means that the request has been paid successfully.

Melting Tokens

For the bolt12 method, the wallet can include an optional outputs field in the melt request to receive change for overpaid Lightning fees (see NUT-08):

{
  "quote": <str>,
  "inputs": <Array[Proof]>,
  "outputs": <Array[BlindedMessage]> // Optional
}

If the outputs field is included and there is excess from the fee_reserve, the mint will respond with a change field containing blind signatures for the overpaid amount:

{
  "quote": <str>,
  "request": <str>,
  "amount": <int>,
  "unit": <str_enum[UNIT]>,
  "fee_reserve": <int>,
  "state": <str_enum[STATE]>,
  "expiry": <int>,
  "payment_preimage": <str>,
  "change": <Array[BlindSignature]> // Present if outputs were included and there's change
}

Example

Melt quote request:

curl -X POST https://mint.host:3338/v1/melt/quote/bolt12 -d \
'{"request": "lno1qcp4256ypqpq86q69t5wv5629arxqurn8cxg9p5qmmqy2e5xq...", "unit": "sat"}'

Melt quote response:

{
  "quote": "TRmjduhIsPxd...",
  "request": "lno1qcp4256ypqpq86q69t5wv5629arxqurn8cxg9p5qmmqy2e5xq...",
  "amount": 10,
  "unit": "sat",
  "fee_reserve": 2,
  "state": "UNPAID",
  "expiry": 1701704757
}

Check quote state:

curl -X GET http://localhost:3338/v1/melt/quote/bolt12/TRmjduhIsPxd...

Melt request:

curl -X POST https://mint.host:3338/v1/melt/bolt12 -d \
'{"quote": "TRmjduhIsPxd...", "inputs": [...]}'

Successful melt response:

{
  "quote": "TRmjduhIsPxd...",
  "request": "lno1qcp4256ypqpq86q69t5wv5629arxqurn8cxg9p5qmmqy2e5xq...",
  "amount": 10,
  "unit": "sat",
  "fee_reserve": 2,
  "state": "PAID",
  "expiry": 1701704757,
  "payment_preimage": "c5a1ae1f639e1f4a3872e81500fd028bece7bedc1152f740cba5c3417b748c1b"
}

Example MeltMethodSetting

{
  "method": "bolt12",
  "unit": <str>,
  "min_amount": <int|null>,
  "max_amount": <int|null>
}