Skip to content
2 changes: 2 additions & 0 deletions 04.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Method-specific NUTs describe how to handle different payment methods. The curre

- [NUT-23][23] for bolt11 Lightning invoices
- [NUT-25][25] for bolt12 Lightning offers
- [NUT-26][26] for onchain Bitcoin transactions

## General Flow

Expand Down Expand Up @@ -160,3 +161,4 @@ Upon receiving the `BlindSignatures` from the mint, the wallet unblinds them to
[20]: 20.md
[23]: 23.md
[25]: 25.md
[26]: 26.md
2 changes: 2 additions & 0 deletions 05.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Method-specific NUTs describe how to handle different payment methods. The curre

- [NUT-23][23] for bolt11 Lightning invoices
- [NUT-25][25] for bolt12 Lightning offers
- [NUT-26][26] for onchain Bitcoin transactions

## General Flow

Expand Down Expand Up @@ -156,3 +157,4 @@ The mint's settings for this NUT indicate the supported method-unit pairs for me
[12]: 12.md
[23]: 23.md
[25]: 25.md
[26]: 26.md
339 changes: 339 additions & 0 deletions 26.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,339 @@
# NUT-26: Onchain

`optional`

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

---

This document describes minting and melting ecash with the `onchain` payment method, which uses Bitcoin onchain transactions. It is an extension of [NUT-04][04] and [NUT-05][05] which cover the protocol steps of minting and melting ecash shared by any supported payment method.

## Mint Quote

For the `onchain` method, the wallet includes the following specific `PostMintQuoteOnchainRequest` data:

```json
{
"unit": <str_enum[UNIT]>,
"pubkey": <str>
}
```

> **Note:** A [NUT-20][20] `pubkey` 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 `PostMintQuoteOnchainResponse`:

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

Where:

- `quote` is the quote ID
- `request` is the Bitcoin address to send funds to
- `expiry` is the Unix timestamp until which the mint quote is valid
- `pubkey` is the public key from the request
- `amount_paid` is the total confirmed amount paid to the request
- `amount_issued` is the amount of ecash that has been issued for the given mint quote

### Example

**1. Create mint quote:**

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

**Response:**

```json
{
"quote": "DSGLX9kevM...",
"request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"unit": "sat",
"expiry": 1701704757,
"pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
"amount_paid": 0,
"amount_issued": 0
}
```

**2. Send payment to the Bitcoin address and check quote state:**

```bash
curl -X GET http://localhost:3338/v1/mint/quote/onchain/DSGLX9kevM...
```

**Response after payment reaches the required number of confirmations:**

> **Note:** The quote state will only update to show `amount_paid` once the Bitcoin transaction has reached the minimum number of confirmations specified in the mint's settings.

```json
{
"quote": "DSGLX9kevM...",
"request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"unit": "sat",
"expiry": 1701704757,
"pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
"amount_paid": 100000,
"amount_issued": 0
}
```

**3. Mint tokens using the confirmed quote:**

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

**Response:**

```json
{
"signatures": [
{
"id": "009a1f293253e41e",
"amount": 50000,
"C_": "0224f1c4c564230ad3d96c5033efdc425582a5a7691d600202732edc6d4b1ec"
},
{
"id": "009a1f293253e41e",
"amount": 50000,
"C_": "0277d1de806ed177007e5b94a8139343b6382e472c752a74e99949d511f7194f6c"
}
]
}
```

## Multiple Deposits

Onchain addresses can receive multiple payments, allowing the wallet to make multiple deposits for one quote. The wallet can call the check onchain endpoint, where the mint will return the `PostMintQuoteOnchainResponse` including `amount_paid` and `amount_issued`. The difference between `amount_paid` and `amount_issued` represents how much the wallet can mint by calling the mint endpoint.

### Example - Additional Payment

Continuing the previous example, if another payment is sent to the same address `bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh` and reaches the required confirmations:

```bash
curl -X GET http://localhost:3338/v1/mint/quote/onchain/DSGLX9kevM...
```

**Response after second payment is confirmed:**

```json
{
"quote": "DSGLX9kevM...",
"request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"unit": "sat",
"expiry": 1701704757,
"pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
"amount_paid": 150000,
"amount_issued": 100000
}
```

The wallet can now mint an additional ₿50,000 worth of ecash.

## Mint Settings

A `confirmations` option **SHOULD** be set to indicate the minimum depth in the blockchain for a transaction to be considered confirmed.

### Example `MintMethodSetting`

```json
{
"method": "onchain",
"unit": <str>,
"min_amount": <int|null>,
"max_amount": <int|null>,
"options": {
"confirmations": <int>
}
}
```

## Melt Quote

For the `onchain` method, the wallet includes the following specific `PostMeltQuoteOnchainRequest` data:

```json
{
"request": <str>,
"unit": <str_enum[UNIT]>,
"amount": <int>
}
```

Where:

- `request` is the Bitcoin address to send to
- `unit` is the unit the wallet would like to pay with
- `amount` is the amount to send in the specified unit

The mint responds with an array of `PostMeltQuoteOnchainResponse`:

```json
[
{
"quote": <str>,
"request": <str>,
"amount": <int>,
"unit": <str_enum[UNIT]>,
"fee": <int>,
"estimated_blocks": <int>,
"state": <str_enum[STATE]>,
"expiry": <int>,
"outpoint": <str|null>
}
]
```

The mint can return multiple `PostMeltQuoteOnchainResponse` with different `fee` amounts and `estimated_blocks`. The wallet can choose which quote to use for melting, and the other quotes will expire.

Where `fee` is the additional fee reserve required to broadcast the transaction, and `estimated_blocks` is the estimated number of blocks until confirmation. The mint expects the wallet to include `Proofs` of _at least_ `total_amount = amount + fee + input_fee` where `input_fee` is calculated from the keyset's `input_fee_ppk` as described in [NUT-02][02].

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

- `"UNPAID"` means that the transaction has not been broadcast yet.
- `"PENDING"` means that the transaction is being processed by the mint but not yet mined.
- `"PAID"` means that the transaction has been mined.

`outpoint` is the transaction ID and output index of the payment in the format `txid:vout`, present once the transaction has been broadcast.

### Melting Tokens

> **NOTE**: For the `onchain` method, `fee` is the absolute fee of the melt quote and the mint will not return [NUT-08][08] change outputs.

### Example

**Melt quote request**:

```bash
curl -X POST https://mint.host:3338/v1/melt/quote/onchain -d \
'{"request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh", "unit": "sat", "amount": 100000}'
```

**Melt quote response**:

```json
[
{
"quote": "TRmjduhIsPxd...",
"request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"amount": 100000,
"unit": "sat",
"fee": 5000,
"estimated_blocks": 1,
"state": "UNPAID",
"expiry": 1701704757
},
{
"quote": "OewtRaqeXmzK...",
"request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"amount": 100000,
"unit": "sat",
"fee": 2000,
"estimated_blocks": 6,
"state": "UNPAID",
"expiry": 1701704757
},
{
"quote": "KfPqNghzLvtY...",
"request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"amount": 100000,
"unit": "sat",
"fee": 800,
"estimated_blocks": 144,
"state": "UNPAID",
"expiry": 1701704757
}
]
```

Check quote state:

```bash
curl -X GET http://localhost:3338/v1/melt/quote/onchain/TRmjduhIsPxd...
```

**Melt request**:

```bash
curl -X POST https://mint.host:3338/v1/melt/onchain -d \
Copy link
Collaborator

@thesimplekid thesimplekid Aug 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to revist this for onchain and we should just do it for all of them while were at it. #37.

But for onchain specifically it make no sense to wait for the melt to confirm and request should return okay once it is received.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to say that a melt quote does not complete until the transaction is confirmed, then yeah. If a completed melt quote means the transaction was broadcast, then it seems fine to wait.

Was that the consensus that the quote stays pending until the transaction is confirmed?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to say that a melt quote does not complete until the transaction is confirmed, then yeah. If a completed > melt quote means the transaction was broadcast, then it seems fine to wait.

What if a mint wants to batch then it doesn't work for broadcast even.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to revist this for onchain and we should just do it for all of them while were at it.

Is there a change you suggest I make here? In NUT-05 it says:

"For methods that involve external payments (like Lightning), this call may block until the payment either succeeds or fails. This can take a long time. Make sure to use no (or a very long) timeout when making this call!"

The way this nut is now seems fine as it shows that a "successful melt response" returns the quote as PENDING and NUT-05 is pretty open-ended in that it says, "this call may block".

'{"quote": "TRmjduhIsPxd...", "inputs": [...]}'
```

**Successful melt response**:

```json
{
"quote": "TRmjduhIsPxd...",
"request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"amount": 100000,
"unit": "sat",
"fee": 5000,
"estimated_blocks": 1,
"state": "PENDING",
"expiry": 1701704757,
"outpoint": "3b7f3b85c5f1a3c4d2b8e9f6a7c5d8e9f1a2b3c4d5e6f7a8b9c1d2e3f4a5b6c7:2"
}
```

**Final confirmed state**:

```json
{
"quote": "TRmjduhIsPxd...",
"request": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"amount": 100000,
"unit": "sat",
"fee": 5000,
"estimated_blocks": 1,
"state": "PAID",
"expiry": 1701704757,
"outpoint": "3b7f3b85c5f1a3c4d2b8e9f6a7c5d8e9f1a2b3c4d5e6f7a8b9c1d2e3f4a5b6c7:2"
}
```

### Example `MeltMethodSetting`

```json
{
"method": "onchain",
"unit": <str>,
"min_amount": <int|null>,
"max_amount": <int|null>,
}
```

[02]: 02.md
[04]: 04.md
[05]: 05.md
[08]: 08.md
[20]: 20.md
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio
| [23][23] | Payment Method: BOLT11 | [Nutshell][py], [cdk-cli] | [Nutshell][py], [cdk-mintd], [nutmix] |
| [24][24] | HTTP 402 Payment Required | - | - |
| [25][25] | Payment Method: BOLT12 | [cdk-cli], [cashu-ts][ts] | [cdk-mintd] |
| [26][26] | Payment Method: Onchain | [cdk-cli], [cashu-ts][ts] | [cdk-mintd] |

#### Wallets:

Expand Down Expand Up @@ -99,3 +100,4 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio
[23]: 23.md
[24]: 24.md
[25]: 25.md
[26]: 26.md