Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
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
32 changes: 20 additions & 12 deletions 29.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ This spec describes how a wallet can mint multiple quotes in one batched operati

## 1. Batch Checking Mint Quotes

Before minting, the wallet SHOULD verify each mint quote's current accounting state. It does this by sending:
Before minting, the wallet SHOULD verify each mint quote's current accounting data. It does this by sending:

```http
POST https://mint.host:3338/v1/mint/quote/{method}/check
Expand All @@ -28,13 +28,19 @@ The wallet includes the following body in its request:

where `quotes` is an array of _unique_ mint quote IDs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

For the check endpoint specifically: is an empty quotes array allowed (→ empty response)? And are duplicate IDs rejected like in batch mint (11016), or answered positionally? Both are undefined for check.


The mint returns a JSON array of mint quote objects as defined by the payment method's NUT specification. The quotes in this array MUST be in the same order as in the request.
The mint returns a JSON array with exactly one entry per requested quote ID, in request order. Entry `i` corresponds to `quotes[i]`, and its `quote` field **MUST** equal the requested ID.

#### Example
Known quotes are returned as mint quote objects as defined by the payment method's NUT specification. A quote ID that the mint does not know or cannot parse is returned as an [unknown entry](#unknown-or-malformed-quote-ids).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Worth being explicit about what "mint quote objects as defined by the payment method's NUT" includes here. Returning the full object (incl. request) makes this endpoint an efficient existence oracle: per-entry unknowns leak ~N bits per query vs ~1 bit with all-or-nothing, and NUT-04 only makes UUIDv7 a SHOULD. Either require strong quote IDs for mints advertising NUT-29, or return only the accounting subset (quote, amount_paid, amount_issued, updated_at) — the test vector already shows that minimal shape.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

With real-life rate limits and mints already shipping UUIDv7 I dont think that is an issue. We previously discussed this. We could change to UUIDv7 to MUST though just to be explicit


Mints **MUST** return the quote object for every quote ID they hold a record of, including quotes whose `expiry` has passed. An unknown entry indicates only that the mint holds no record of the ID.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What about an ID the mint does hold, but under a different method (a bolt12 ID sent to /quote/bolt11/check), or a melt quote? Returning the object leaks cross-method existence; returning unknown contradicts "holds no record of the ID". Suggest specifying the endpoint is scoped to {method} and anything outside that scope MUST be returned as unknown.


Mints **MUST NOT** delete a quote for which ecash is still owed (`amount_paid - amount_issued > 0`).

### Example

Below is an example for checking two bolt11 mint quotes.

##### Request
#### Request

```http
POST https://mint.host:3338/v1/mint/quote/bolt11/check
Expand All @@ -45,7 +51,7 @@ Content-Type: application/json
}
```

##### Response
#### Response

```json
[
Expand All @@ -55,7 +61,6 @@ Content-Type: application/json
"amount_paid": 100,
"amount_issued": 0,
"updated_at": 1234567800,
"state": "PAID",
"unit": "sat",
"amount": 100,
"expiry": 1234567890
Expand All @@ -66,20 +71,23 @@ Content-Type: application/json
"amount_paid": 0,
"amount_issued": 0,
"updated_at": 1234567800,
"state": "UNPAID",
"unit": "sat",
"amount": 50,
"expiry": 1234567890
}
]
```

#### Error Handling
### Unknown or Malformed Quote IDs

This is a query endpoint that uses all-or-nothing error handling, matching the behavior of the batch mint endpoint:
This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `unknown` entry for it as below and continue processing the remaining IDs.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I wonder if we should name the return type, as we do for other response shapes.

Suggested change
This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `unknown` entry for it as below and continue processing the remaining IDs.
This is a query endpoint and does not use the all-or-nothing error handling of the batch mint endpoint. If a quote ID is unknown or cannot be parsed, the mint **MUST** return an `UnknownQuote` for it as below and continue processing the remaining IDs.


- If any `quote_id` is not known by the mint, the mint MUST reject the entire request and return an appropriate error
- If any `quote_id` cannot be parsed (invalid format), the mint MUST reject the entire request and return an appropriate error
```json
{
"quote": <str>,
"unknown": true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggest stating that unknown entries contain exactly quote and unknown: true, so wallets have a clean discriminator and an entry can't be both a full quote object and unknown.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

```

---
Comment thread
Egge21M marked this conversation as resolved.

Expand Down Expand Up @@ -192,7 +200,7 @@ The mint MUST validate the following before processing a batch mint request:
3. **Valid quote IDs**: All quote IDs MUST exist in the mint's database
4. **Payment method consistency**: All quotes MUST have the same payment method, matching `{method}` in the URL path
5. **Currency unit consistency**: All quotes MUST use the same currency unit
6. **Quote state**: All quotes MUST be in PAID state (or have a mintable amount for payment methods that allow multiple mint operations like bolt12)
6. **Mintable amount**: All quotes MUST have a positive currently mintable amount (`amount_paid - amount_issued`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dropping the PAID-state rule also drops any mention of expiry from mint validation: nothing in this list says whether an expired (but paid) quote can still be batch-minted. Some mints will reject, some won't — should be stated explicitly, especially since the check section now requires returning expired quotes.

7. **Amount balance**: The sum of amounts contained in the `outputs` MUST equal the sum of `quote_amounts` (bolt11) or MUST NOT exceed it (bolt12)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With rule 6 loosened to "positive mintable amount", a batch can now include an already partially-minted bolt11 quote — but this equality check doesn't say what the target is in that case: the quote's original amount, or its currently mintable amount (amount_paid - amount_issued)? If it's the original amount, a mint can be pushed past NUT-04's cap. Worth defining, including what quote_amounts means for a partially-issued quote.

8. **Signature validation (NUT-20)**: The `signatures` array length MUST match the `quotes` array length; locked quotes MUST include a valid signature; unlocked quotes MUST NOT include one

Expand Down
32 changes: 23 additions & 9 deletions tests/29-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,37 @@ The following is the corresponding response with a blind signature.
}
```

## Check endpoint with unknown quotes
## Check endpoint omits unknown and malformed quotes
Comment thread
Egge21M marked this conversation as resolved.
Outdated

The following is an invalid check request containing an unknown quote ID.
The following check request contains two known quote IDs, one malformed quote ID, and one unknown quote ID.

```json
{ "quotes": ["known-1", "bogus", "unknown-2"] }
{ "quotes": ["known-1", "not-a-valid-quote-id", "unknown-2", "known-2"] }
```

Per NUT-29, quote check uses all-or-nothing error handling. If any quote is unknown, the entire request must be rejected.
The mint omits the malformed and unknown quote IDs. It returns the known quotes in the same relative order as their IDs in the request.
Comment thread
Egge21M marked this conversation as resolved.
Outdated

```json
{
"code": "UNKNOWN_QUOTE",
"error": "one or more quote IDs are unknown"
}
[
{
"quote": "known-1",
"amount_paid": 5,
"amount_issued": 0,
"updated_at": 1234567800
},
{ "quote": "not-a-valid-quote-id", "unknown": true },
{ "quote": "unknown-2", "unknown": true },
{
"quote": "known-2",
"amount_paid": 0,
"amount_issued": 0,
"updated_at": 1234567800
}
]
Comment thread
Egge21M marked this conversation as resolved.
```

If the mint cannot handle any of the requested quote IDs, it returns an empty JSON array.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
If the mint cannot handle any of the requested quote IDs, it returns an empty JSON array.
If the mint cannot handle any of the requested quote IDs, every entry in the response is an `unknown` entry. The response is never shorter than the request.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

In CTS, I have a failIf(quotes.length != responses.length), so if the view is to make an exception for fully failed lookup, I will need to loosen that.

@a1denvalu3 a1denvalu3 Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This contradicts 29.md: the spec now says a successful response always has exactly one entry per requested ID, but this line still says an empty array is returned. An empty response to a non-empty request breaks the positional contract. Shouldn't this read that every entry is an unknown entry (response never shorter than the request)?

EDIT: just needs to be updated ig


## Batch mint atomic failure

The following is an invalid batch mint request containing one unknown quote ID, causing the entire batch to fail atomically with no partial minting.
Expand All @@ -52,7 +66,7 @@ Expected behavior:

- The mint rejects the whole request with an error.
- No outputs are signed.
- No quote state is consumed/changed by partial processing.
- No quote's `amount_issued` is increased by partial processing.

## Batch mint rejects empty quotes array

Expand Down
Loading