{"id":"106f12c83c2f0e4da336e98ccedb3b89133744694c42ac8fbf68c568eec5e440","pubkey":"d1678e7ef965374bbea308a1215609a78376dc158277a7d657680f9d5efd5c38","created_at":1785172959,"kind":30817,"tags":[["d","nut-29"],["title","NUT-29: Batched Minting"],["summary","Minting several quotes in one atomic request."],["s","optional"],["t","cashu"],["t","ecash"],["t","nut"],["alt","A specification: NUT-29: Batched Minting"],["client","openspecs-import"],["published_at","1773051589"],["proxy","https://github.com/cashubtc/nuts/blob/a845dfc998abae501fc3419592d53dc995d34b12/29.md","web"],["x","a029c253f002dd37559fe26d716f8d6665e7710134ab6968aff39c5ba152bec0"]],"content":"# NUT-29: Batched Minting\n\n`optional`\n\n`depends on: NUT-04`\n\n`uses: NUT-20`\n\nThis spec describes how a wallet can mint multiple quotes in one batched operation by requesting blind signatures for multiple quotes in a single atomic request.\n\n---\n\n## 1. Batch Checking Mint Quotes\n\nBefore minting, the wallet SHOULD verify each mint quote's current accounting state. It does this by sending:\n\n```http\nPOST https://mint.host:3338/v1/mint/quote/{method}/check\n```\n\nThe wallet includes the following body in its request:\n\n```json\n{\n  \"quotes\": <Array[str]>\n}\n```\n\nwhere `quotes` is an array of _unique_ mint quote IDs.\n\nThe 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.\n\n#### Example\n\nBelow is an example for checking two bolt11 mint quotes.\n\n##### Request\n\n```http\nPOST https://mint.host:3338/v1/mint/quote/bolt11/check\nContent-Type: application/json\n\n{\n  \"quotes\": [ \"019e6d5a-2347-7000-8037-b42dae20f1fe\", \"019e6d5a-2347-7000-868a-08e59a1c6716\" ]\n}\n```\n\n##### Response\n\n```json\n[\n  {\n    \"quote\": \"019e6d5a-2347-7000-8037-b42dae20f1fe\",\n    \"request\": \"lnbc...\",\n    \"amount_paid\": 100,\n    \"amount_issued\": 0,\n    \"updated_at\": 1234567800,\n    \"state\": \"PAID\",\n    \"unit\": \"sat\",\n    \"amount\": 100,\n    \"expiry\": 1234567890\n  },\n  {\n    \"quote\": \"019e6d5a-2347-7000-868a-08e59a1c6716\",\n    \"request\": \"lnbc...\",\n    \"amount_paid\": 0,\n    \"amount_issued\": 0,\n    \"updated_at\": 1234567800,\n    \"state\": \"UNPAID\",\n    \"unit\": \"sat\",\n    \"amount\": 50,\n    \"expiry\": 1234567890\n  }\n]\n```\n\n#### Error Handling\n\nThis is a query endpoint that uses all-or-nothing error handling, matching the behavior of the batch mint endpoint:\n\n- If any `quote_id` is not known by the mint, the mint MUST reject the entire request and return an appropriate error\n- If any `quote_id` cannot be parsed (invalid format), the mint MUST reject the entire request and return an appropriate error\n\n---\n\n## 2. Executing the Batched Mint\n\n#### Request by wallet\n\nOnce all quoted payments are confirmed, the wallet mints the proofs by calling:\n\n```http\nPOST https://mint.host:3338/v1/mint/{method}/batch\n```\n\nThe batch endpoint is method-specific: every quote in the batch **MUST** be for the same payment `method` as the `{method}` in the URL, and a batch that mixes payment methods **MUST** be rejected by the mint.\n\nThe wallet includes the following body in its request:\n\n```json\n{\n  \"quotes\": <Array[str]>,\n  \"quote_amounts\": <Array[int]|null>, // Optional\n  \"outputs\": <Array[BlindedMessage]>,\n  \"signatures\": <Array[string]|null> // Optional\n}\n```\n\n- `quotes`: array of _unique_ quote IDs.\n- `quote_amounts`: array of expected amounts to mint per quote, in the same order as `quotes`.\n  - Required for payment methods that demand an amount like bolt12; Optional for other methods like bolt11.\n- `outputs`: array of blinded messages (see [NUT-00][00]).\n- `signatures`: array of signatures for NUT-20 locked quotes. See [NUT-20 Support][nut-20-support]\n\n#### Response by mint\n\nThe mint responds with:\n\n```json\n{\n  \"signatures\": <Array[BlindSignature]>\n}\n```\n\n- `signatures`: an array of blind signatures, one for each provided blinded message, in the same order as the `outputs` array.\n\n### Example\n\nBelow is an example for minting two NUT-20 locked bolt11 mint quotes.\n\n##### Request\n\n```http\nPOST https://mint.host:3338/v1/mint/bolt12/batch\nContent-Type: application/json\n\n{\n  \"quotes\": [\n    \"019e6d5a-2347-7000-8037-b42dae20f1fe\",\n    \"019e6d5a-2347-7000-868a-08e59a1c6716\"\n  ],\n  \"quote_amounts\": [\n    100,\n    50\n  ],\n  \"signatures\": [\n    \"d9be080b33179387e504bb6991ea41ae0dd715e28b01ce9f63d57198a095bccc776874914288e6989e97ac9d255ac667c205fa8d90a211184b417b4ffdd24092\",\n    \"f2d97118390195cf5bef21d84c94e505dcdc2760154519536f74ba5e27f886f313b82296610df14db1d91d346e988ed384070bad084aaf06d14ccd7686157f24\"\n  ],\n  \"outputs\": [\n    {\n      \"amount\": 128,\n      \"id\": \"009a1f293253e41e\",\n      \"B_\": \"035015e6d7ade60ba8426cefaf1832bbd27257636e44a76b922d78e79b47cb689d\"\n    },\n    {\n      \"amount\": 16,\n      \"id\": \"009a1f293253e41e\",\n      \"B_\": \"0288d7649652d0a83fc9c966c969fb217f15904431e61a44b14999fabc1b5d9ac6\"\n    },\n    {\n      \"amount\": 4,\n      \"id\": \"009a1f293253e41e\",\n      \"B_\": \"03b85be0c0a9f51056375b632a2f2c8149831b9827fad677be9807455e7d84b584\"\n    },\n    {\n      \"amount\": 2,\n      \"id\": \"009a1f293253e41e\",\n      \"B_\": \"0276bbcf69e1b1238e6d39fc14c84e7cdfd519fee07f3369d2b2b23045390e2efc\"\n    }\n  ]\n}\n```\n\n##### Response\n\n```json\n{\n  \"signatures\": [\n    \"0208657b2917f469f275226cc931e5389451f6eed515d586ad16fa7a700eed4fb6\",\n    \"037dc0b5e712a39ef22f5bba1d49bc65a323ab9e0b771f7281d8c33280e2e58dbc\"\n  ]\n}\n```\n\n### Request Validation\n\nThe mint MUST validate the following before processing a batch mint request:\n\n1. **Non-empty batch**: The `quotes` array MUST NOT be empty\n2. **Unique quotes**: All quote IDs in the `quotes` array MUST be unique (no duplicates) — error code `11016`\n3. **Valid quote IDs**: All quote IDs MUST exist in the mint's database\n4. **Payment method consistency**: All quotes MUST have the same payment method, matching `{method}` in the URL path\n5. **Currency unit consistency**: All quotes MUST use the same currency unit\n6. **Quote state**: All quotes MUST be in PAID state (or have a mintable amount for payment methods that allow multiple mint operations like bolt12)\n7. **Amount balance**: The sum of amounts contained in the `outputs` MUST equal the sum of `quote_amounts` (bolt11) or MUST NOT exceed it (bolt12)\n8. **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\n\nImplementations MAY impose additional constraints such as maximum batch size based on their resource limitations. If any validation fails, the mint MUST reject the entire batch and return an appropriate error without minting any quotes.\n\n### NUT-20 support\n\nPer [NUT-20][20], quotes can require authentication via signatures. When using batch minting with NUT-20 locked quotes:\n\n#### Signature Array Structure\n\n**Array structure:**\n\n- The `signatures` field is an array with length equal to `quotes.length` (one entry per quote)\n- `signatures[i]` corresponds to `quotes[i]`\n\n**Per-quote signatures:**\n\n- **Locked quotes** (with `pubkey`): `signatures[i]` contains the signature string\n- **Unlocked quotes**: `signatures[i]` is `null`\n\n**Field requirement:**\n\n- **Required**: If ANY quote is locked\n- **Optional**: May be omitted entirely if all quotes are unlocked\n\n#### Signature Message\n\nEach locked quote is signed independently exactly as in [NUT-20][20]: `signatures[i]` is the NUT-20 signature over `quotes[i]` and the request's `outputs`. The batch `outputs` are a single consolidated set (not partitioned per quote), so each signature is computed over the full `outputs` array.\n\nFollowing the [NUT-20 message aggregation][20-msg-agg] pattern, the signature message for `quotes[i]` is computed as:\n\n```\nmsg_to_sign = b\"Cashu_MintQuoteSig_v1\"\n              || len32(quotes[i]) || quotes[i]\n              || for each output j (in request order):\n                   len32(amount_j) || amount_j\n                   || len32(B_j)   || B_j\n```\n\nWhere:\n\n- `b\"Cashu_MintQuoteSig_v1\"` is the domain-separation tag as raw ASCII bytes, not length-prefixed.\n- `quotes[i]` is the UTF-8 encoded quote ID from the request's `quotes` array at index `i`\n- `outputs` are **all blinded messages** from the request's `outputs` array (regardless of which quote they correspond to)\n- `amount_j` is the output amount as canonical minimal big-endian bytes (e.g. `0` → empty byte array, `1` → `0x01`, `256` → `0x0100`); thus `len32(amount_j)` is its length in bytes as a 32-bit integer (e.g. `0` for amount `0`, `1` for amount `1`, `2` for amount `256`).\n- `B_j` is the raw byte representation of the blinded message (e.g. 33-byte compressed secp256k1 or 48-byte BLS12-381 point), decoded from the request's hex string.\n- `||` denotes byte concatenation and `len32(x)` is the 32-bit (4-byte) big-endian length of the following data `x`.\n\n### Signature Validation Failure\n\nIf **any signature in the batch is invalid**, the mint MUST reject the **entire batch** and return an error. This maintains atomicity: all quotes must be successfully authenticated and minted together, or none at all.\n\n### Example\n\n```json\n{\n  \"quotes\": [\n    \"019e6d5a-2347-7000-8005-9428c4fc5edb\",\n    \"019e6d5a-2347-7000-841a-4d222fd00a33\",\n    \"019e6d5a-2347-7000-8d60-d35c1a9e19b3\"\n  ],\n  \"outputs\": [\n    { \"amount\": 64, \"id\": \"keyset_1\", \"B_\": \"...\" },\n    { \"amount\": 64, \"id\": \"keyset_1\", \"B_\": \"...\" },\n    { \"amount\": 22, \"id\": \"keyset_1\", \"B_\": \"...\" }\n  ],\n  \"signatures\": [\n    \"d9be080b...\", // Signature for quote[0], covers ALL 3 outputs\n    null, // Quote[1] is unlocked\n    \"a1c5f7e2...\" // Signature for quote[2], covers ALL 3 outputs\n  ]\n}\n```\n\n## Implementation Notes\n\n### Batch Size Limits\n\nMints MAY advertise a maximum batch size through the [NUT-06][06] mint info endpoint. The batch size limit is included in the `nuts` object under the `29` key:\n\n```json\n{\n  \"nuts\": {\n    \"29\": {\n      \"max_batch_size\": 100,\n      \"methods\": [\"bolt11\", \"bolt12\"]\n    }\n  }\n}\n```\n\nFields:\n\n- `max_batch_size` (optional): Maximum number of quotes allowed in a single batch request. If omitted, the batch size limit is implementation-defined and clients MUST handle error code `11017` gracefully.\n- `methods` (optional): Array of payment methods supported for batch minting. If omitted, all methods supported by the mint (per NUT-04) are available for batching.\n\n[00]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crqqpd6d9\n[06]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crv3ltsuz\n[20]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595erqzryd5a\n[20-msg-agg]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595erqzryd5a#message-aggregation\n[nut-20-support]: #nut-20-support\n","sig":"06d1f2e02714225a6c2e58dbecaf80083fec0d354e3346d25a05ec46b3d04dab5dc79ab676b90afd63449a30ac9791b5d8daf6b3ec698c8fa614087c59486b26"}