Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
320 changes: 320 additions & 0 deletions 28.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,320 @@
# NUT-28: Third-Party Mint Quotes

`optional`

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

---

This NUT enables third-party mint quotes where one user (`Alice`) creates and pays for a mint quote that only another user (`Bob`) can redeem. When creating a mint quote, `Alice` provides `Bob`'s public key. The mint will then allow only `Bob` to discover and redeem the quote by signing requests with the corresponding private key.

> [!NOTE]
>
> This NUT extends [NUT-20][20] to support payer-initiated, recipient-locked quotes. `Alice` uses `Bob`'s public key when creating the quote (instead of her own), and `Bob` can query the mint to discover quotes assigned to his public key.

## Use cases

- **Mining pools**: A pool creates paid quotes for miners' public keys when shares are validated
- **Gift payments**: `Alice` pays for `Bob` to receive ecash without `Bob` initiating a request
- **Batch payouts**: Services create multiple paid quotes for different recipients

## Quote creation

To create a third-party mint quote, `Alice` makes a `POST /v1/mint/quote/{method}` request with `Bob`'s public key. We present an example with the `method` being `bolt11` here.

```http
POST https://mint.host:3338/v1/mint/quote/bolt11
```

`Alice` includes the following `PostMintQuoteBolt11Request` data in her request:

```json
{
"amount": <int>,
"unit": <str_enum[UNIT]>,
"description": <str>, // Optional
"pubkey": <str> // Bob's pubkey
}
```

with the requested `amount`, `unit`, and `description` according to [NUT-04][04] and [NUT-23][23].

`pubkey` is `Bob`'s compressed secp256k1 public key (33 bytes, hex-encoded). The mint will require a valid signature from `Bob`'s corresponding private key to process the mint operation.

The mint responds with a `PostMintQuoteBolt11Response` as in [NUT-20][20]:

```json
{
"quote": <str>,
"request": <str>,
"state": <str_enum[STATE]>,
"expiry": <int>,
"pubkey": <str>
}
```
Comment thread
average-gary marked this conversation as resolved.
Outdated

`Alice` then pays the Lightning invoice in `request`. The quote state transitions to `PAID`.

### Example

Request of `Alice` with curl:

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

Response of the mint:

```json
{
"quote": "9d745270-1405-46de-b5c5-e2762b4f5e00",
"request": "lnbc1000n1pj4apw9...",
"amount": 100,
"unit": "sat",
"state": "UNPAID",
"expiry": 1701704757,
"pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac"
}
```

## Querying quotes by public key

`Bob` can query the mint to discover quotes assigned to his public key. The wallet makes a `POST /v1/mint/quote/{method}/pubkey` request.

```http
POST https://mint.host:3338/v1/mint/quote/bolt11/pubkey
```

`Bob` includes the following `PostMintQuotesByPubkeyRequest` data:

```json
{
"pubkey": <str>,
"timestamp": <int>,
"signature": <str>,
"unit": <str|null>,
"state": <str|null>
}
```

| Field | Type | Description |
| --- | --- | --- |
| `pubkey` | `str` | `Bob`'s compressed secp256k1 public key (33 bytes, hex-encoded) |
| `timestamp` | `int` | Unix timestamp of the request (for replay protection) |
| `signature` | `str` | BIP340 Schnorr signature on the message (see below) |
| `unit` | `str\|null` | Optional: Filter by currency unit (e.g., `"sat"`) |
| `state` | `str\|null` | Optional: Filter by quote state (e.g., `"PAID"`) |

### Signature scheme

To query quotes, `Bob` must sign a message proving ownership of the public key. The message to sign is:

```
msg_to_sign = "quote_lookup" || pubkey || timestamp
```

Where `||` denotes concatenation, `"quote_lookup"` is a literal UTF-8 domain separator string, `pubkey` is the hex-encoded public key string, and `timestamp` is the UTF-8 string representation of the Unix timestamp.

The signature is a [BIP340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki) Schnorr signature on the SHA-256 hash of `msg_to_sign`.

### Replay protection

The mint **MUST** implement replay protection:

1. Reject requests where `timestamp` differs from the mint's clock by more than the configured tolerance (default: 60 seconds)
2. Track recent `(pubkey, timestamp)` pairs and reject duplicates within the validity window

> [!TIP]
>
> Wallets can use the `time` field in the mint info response ([NUT-06][06]) to adjust for clock skew.

### Response

The mint responds with a `PostMintQuotesByPubkeyResponse`:

```json
{
"quotes": [
{
"quote": <str>,
"request": <str>,
"amount": <int>,
"unit": <str>,
"state": <str_enum[STATE]>,
"expiry": <int|null>,
"pubkey": <str>
},
...
]
}
```

The response contains an array of quote objects matching the query criteria. If no quotes are found, the array is empty.

### Example

Request of `Bob` with curl:

```bash
curl -X POST http://localhost:3338/v1/mint/quote/bolt11/pubkey -H "Content-Type: application/json" -d \
'{
"pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
"timestamp": 1701704800,
"signature": "d4b386f21f7aa7172f0994ee6e4dd966539484247ea71c99b81b8e09b1bb2acb6e9c2f0e3f9a1b5c8d7e6f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2",
"state": "PAID"
}'
```

Response:

```json
{
"quotes": [
{
"quote": "9d745270-1405-46de-b5c5-e2762b4f5e00",
"request": "lnbc1000n1pj4apw9...",
"amount": 100,
"unit": "sat",
"state": "PAID",
"expiry": 1701704757,
"pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac"
}
]
}
```

## Minting tokens

After discovering a `PAID` quote, `Bob` mints tokens using the standard [NUT-20][20] flow. `Bob` signs the mint request with his private key.

```http
POST https://mint.host:3338/v1/mint/bolt11
```

`Bob` includes the following `PostMintBolt11Request` data:

```json
{
"quote": <str>,
"outputs": <Array[BlindedMessage]>,
"signature": <str>
}
```

The `signature` follows the [NUT-20][20] message aggregation scheme:

```
msg_to_sign = quote || B_0 || ... || B_(n-1)
```

The mint verifies the signature against the `pubkey` stored with the quote and responds with blind signatures as in [NUT-04][04].
Comment thread
average-gary marked this conversation as resolved.
Outdated

### Example

Request of `Bob` with curl:

```bash
curl -X POST https://mint.host:3338/v1/mint/bolt11 -H "Content-Type: application/json" -d \
'{
"quote": "9d745270-1405-46de-b5c5-e2762b4f5e00",
"outputs": [
{
"amount": 64,
"id": "009a1f293253e41e",
"B_": "035015e6d7ade60ba8426cefaf1832bbd27257636e44a76b922d78e79b47cb689d"
},
{
"amount": 32,
"id": "009a1f293253e41e",
"B_": "0288d7649652d0a83fc9c966c969fb217f15904431e61a44b14999fabc1b5d9ac6"
},
{
"amount": 4,
"id": "009a1f293253e41e",
"B_": "02407b3c1d4a8e6f5b9c7d2e1f0a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b"
}
],
"signature": "e5bc2a6f8c..."
}'
```

## State transitions

```
UNPAID ──[Alice pays]──► PAID ──[Bob mints]──► ISSUED
│ │
│ invoice expiry │ keyset final_expiry
▼ ▼
EXPIRED EXPIRED
```

Quote states follow [NUT-04][04]:

- `UNPAID`: Quote created, Lightning invoice not yet paid
- `PAID`: Invoice paid, waiting for recipient to mint
- `ISSUED`: Tokens have been minted
- `EXPIRED`: Quote expired before completion

### Expiry policy

Quotes **SHOULD** respect the keyset's `final_expiry` ([NUT-01][01]). Mints define their own retention policy for `PAID` quotes that have not been redeemed.
Comment thread
average-gary marked this conversation as resolved.
Outdated

## Errors

See [Error Codes][errors]:

- `20010`: Signature for quote lookup invalid
- `20011`: Timestamp for quote lookup outside valid window
- `20012`: Duplicate request (replay detected)

Errors from [NUT-20][20] also apply:

- `20008`: Mint quote with `pubkey` but no valid `signature` provided for mint request
- `20009`: Mint quote requires `pubkey` but none given or invalid `pubkey`

## Settings

The settings for this NUT indicate support for third-party mint quote lookup. They are part of the info response of the mint ([NUT-06][06]) which in this case reads:

```json
{
"28": {
"supported": <bool>,
"methods": <Array[str]>,
"timestamp_tolerance_secs": <int|null>,
"quote_retention_days": <int|null>
}
}
```

| Field | Type | Description |
| --- | --- | --- |
| `supported` | `bool` | Whether third-party quote lookup is supported |
| `methods` | `str[]` | Payment methods supporting third-party quotes (e.g., `["bolt11"]`) |
| `timestamp_tolerance_secs` | `int\|null` | Tolerance window for signature timestamps in seconds (default: 60) |
| `quote_retention_days` | `int\|null` | How long `PAID` quotes are retained in days (`null` = indefinite) |
Comment thread
average-gary marked this conversation as resolved.
Outdated

## Optional: Nostr discovery

`Alice` can notify `Bob` of a paid quote via an encrypted Nostr DM ([NIP-04](https://github.com/nostr-protocol/nips/blob/master/04.md) or [NIP-44](https://github.com/nostr-protocol/nips/blob/master/44.md)):

```json
{
"type": "cashu_third_party_quote",
"mint": "https://mint.host:3338",
"method": "bolt11",
"pubkey": "03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac",
"amount": 100,
"unit": "sat"
}
```
Comment thread
average-gary marked this conversation as resolved.
Outdated

This is optional. The core protocol works with direct mint queries.

[00]: 00.md
[01]: 01.md
[04]: 04.md
[06]: 06.md
[20]: 20.md
[23]: 23.md
[errors]: error_codes.md
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio
| [25][25] | Payment Method: BOLT12 | [cdk], [cashu-ts][ts] | [cdk-mintd] |
| [26][26] | Payment Request Bech32m Encoding | [cdk] | - |
| [27][27] | Nostr Mint Backup | [Cashu.me][cashume], [cdk] | - |
| [28][28] | Third-Party Mint Quotes | - | - |

#### Wallets:

Expand Down Expand Up @@ -102,3 +103,4 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio
[25]: 25.md
[26]: 26.md
[27]: 27.md
[28]: 28.md
4 changes: 4 additions & 0 deletions error_codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
| 20007 | Quote is expired | [NUT-04][04], [NUT-05][05] |
| 20008 | Signature for mint request invalid | [NUT-20][20] |
| 20009 | Pubkey required for mint quote | [NUT-20][20] |
| 20010 | Signature for quote lookup invalid | [NUT-28][28] |
| 20011 | Timestamp for quote lookup outside valid window | [NUT-28][28] |
| 20012 | Duplicate request (replay detected) | [NUT-28][28] |
| 30001 | Endpoint requires clear auth | [NUT-21][21] |
| 30002 | Clear authentication failed | [NUT-21][21] |
| 31001 | Endpoint requires blind auth | [NUT-22][22] |
Expand All @@ -50,3 +53,4 @@
[20]: 20.md
[21]: 21.md
[22]: 22.md
[28]: 28.md
Loading