{"id":"f5d14df8cfc5d8240e651620579ce7c71af4f40bd32d66355d85b922b0a49b65","pubkey":"d1678e7ef965374bbea308a1215609a78376dc158277a7d657680f9d5efd5c38","created_at":1783082300,"kind":30817,"tags":[["d","nut-30"],["title","NUT-30: Payment Method: Onchain"],["summary","Minting and melting with onchain Bitcoin payments."],["s","optional"],["t","cashu"],["t","ecash"],["t","nut"],["alt","A specification: NUT-30: Payment Method: Onchain"],["client","openspecs-import"],["published_at","1779294043"],["proxy","https://github.com/cashubtc/nuts/blob/a845dfc998abae501fc3419592d53dc995d34b12/30.md","web"],["x","8e9fb143ffa98638f29fd6a657c709e89c409f86a257ba5f2315700e750fc985"]],"content":"# NUT-30: Payment Method: Onchain\n\n`optional`\n\n`depends on: NUT-04 NUT-05 NUT-20`\n\n---\n\nThis document describes minting and melting ecash with the `onchain` payment method, which uses Bitcoin onchain payments. 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.\n\n## Mint Quote\n\nFor the `onchain` method, the wallet includes the following specific `PostMintQuoteOnchainRequest` data:\n\n```json\n{\n  \"unit\": <str_enum[UNIT]>,\n  \"pubkey\": <str>\n}\n```\n\n> **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.\n\nThe mint responds with a `PostMintQuoteOnchainResponse`:\n\n```json\n{\n  \"quote\": <str>,\n  \"request\": <str>,\n  \"unit\": <str_enum[UNIT]>,\n  \"method\": \"onchain\",\n  \"expiry\": <int|null>,\n  \"pubkey\": <str>,\n  \"amount_paid\": <int>,\n  \"amount_issued\": <int>,\n  \"updated_at\": <int>\n}\n```\n\nWhere:\n\n- `quote` is the quote ID\n- `request` is the Bitcoin address to send funds to\n- `expiry` is the Unix timestamp until which the mint quote is valid\n- `pubkey` is the public key from the request\n- `amount_paid` is the total confirmed amount paid to the request in UTXOs that are eligible for minting\n- `amount_issued` is the amount of ecash that has been issued for the given mint quote\n- `updated_at` is defined in [NUT-04][04]\n\nIf `expiry` is not `null`, the wallet **SHOULD NOT** send payments to the request after `expiry`. Mints **MUST** keep monitoring transactions they detected before `expiry` until the transaction reaches the required number of confirmations or is evicted or replaced. Payments first detected by the mint after `expiry` **MUST NOT** increase `amount_paid`.\n\n### Example\n\n**Request** with curl:\n\n```bash\ncurl -X POST http://localhost:3338/v1/mint/quote/onchain -d \\\n'{\"unit\": \"sat\", \"pubkey\": \"03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac\"}' \\\n-H \"Content-Type: application/json\"\n```\n\n**Response**:\n\n```json\n{\n  \"quote\": \"019e6d5a-2347-7000-8850-39c85ed1b5d3\",\n  \"request\": \"bc1q...\",\n  \"unit\": \"sat\",\n  \"method\": \"onchain\",\n  \"expiry\": 1701704757,\n  \"pubkey\": \"03d56ce4e446a85bbdaa547b4ec2b073d40ff802831352b8272b7dd7a4de5a7cac\",\n  \"amount_paid\": 0,\n  \"amount_issued\": 0,\n  \"updated_at\": 1701704657\n}\n```\n\nCheck mint quote:\n\n```bash\ncurl -X GET http://localhost:3338/v1/mint/quote/onchain/019e6d5a-2347-7000-8850-39c85ed1b5d3\n```\n\n### Minting Tokens\n\nThe quote accounting will only update to show `amount_paid` once a Bitcoin transaction has reached the minimum number of confirmations specified in the mint's settings.\n\nIf the `onchain` mint method has a `min_amount` setting, each UTXO paid to the quote address is evaluated independently against `min_amount`. UTXOs with an amount less than `min_amount` **MUST NOT** increase `amount_paid` and **MUST NOT** count towards the mintable balance for the quote. Multiple UTXOs below `min_amount` **MUST NOT** be aggregated to reach `min_amount`.\n\nFor the `onchain` method, the wallet includes the following specific `PostMintOnchainRequest` data:\n\n```json\n{\n  \"quote\": <str>,\n  \"outputs\": <Array[BlindedMessage]>,\n  \"signature\": <str>\n}\n```\n\nSince onchain mint quotes require a `pubkey`, the wallet **MUST** include a [NUT-20][20] `signature` in the mint request.\n\nMinting tokens:\n\n```bash\ncurl -X POST https://mint.host:3338/v1/mint/onchain -H \"Content-Type: application/json\" -d \\\n'{\n  \"quote\": \"019e6d5a-2347-7000-8850-39c85ed1b5d3\",\n  \"outputs\": [\n    {\n      \"amount\": 8,\n      \"id\": \"009a1f293253e41e\",\n      \"B_\": \"035015e6d7ade60ba8426cefaf1832bbd27257636e44a76b922d78e79b47cb689d\"\n    },\n    {\n      \"amount\": 2,\n      \"id\": \"009a1f293253e41e\",\n      \"B_\": \"0288d7649652d0a83fc9c966c969fb217f15904431e61a44b14999fabc1b5d9ac6\"\n    }\n  ],\n  \"signature\": \"f2a1...\"\n}'\n```\n\nResponse:\n\n```json\n{\n  \"signatures\": [\n    {\n      \"id\": \"009a1f293253e41e\",\n      \"amount\": 2,\n      \"C_\": \"0224f1c4c564230ad3d96c5033efdc425582397a5a7691d600202732edc6d4b1ec\"\n    },\n    {\n      \"id\": \"009a1f293253e41e\",\n      \"amount\": 8,\n      \"C_\": \"0277d1de806ed177007e5b94a8139343b6382e472c752a74e99949d511f7194f6c\"\n    }\n  ]\n}\n```\n\n## Multiple Deposits\n\nOnchain addresses can receive multiple payments, allowing the wallet to mint multiple times 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 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`).\n\nOnly eligible UTXOs increase `amount_paid`. If a quote receives both eligible and ineligible UTXOs, only the eligible UTXOs count towards `amount_paid` and mintable balance. UTXOs that do not increase `amount_paid` are not recoverable through the mint quote protocol.\n\n## Mint Settings\n\nA `confirmations` option **SHOULD** be set to indicate the minimum depth in the blockchain for a transaction to be considered confirmed.\n\nFor the `onchain` mint method, `min_amount` indicates both the minimum mint operation amount and the minimum amount of an individual UTXO that the mint will credit to `amount_paid`. Wallets **SHOULD NOT** send onchain payments below `min_amount` to a quote address.\n\n### Example `MintMethodSetting`\n\n```json\n{\n  \"method\": \"onchain\",\n  \"unit\": <str>,\n  \"min_amount\": <int|null>,\n  \"max_amount\": <int|null>,\n  \"options\": {\n    \"confirmations\": <int>\n  }\n}\n```\n\n## Melt Quote\n\nFor the `onchain` method, the wallet includes the following specific `PostMeltQuoteOnchainRequest` data:\n\n```json\n{\n  \"request\": <str>,\n  \"unit\": <str_enum[UNIT]>,\n  \"amount\": <int>\n}\n```\n\nWhere:\n\n- `request` is the Bitcoin address that should receive the onchain payment\n- `unit` is the unit the wallet would like to pay with\n- `amount` is the amount to send in the specified unit\n\nUnlike other melt methods, a single `onchain` melt quote can contain multiple fee options for the same payment. This allows the wallet to choose between different fee and confirmation estimates while preserving a single quote ID.\n\nThe mint responds with a `PostMeltQuoteOnchainResponse`:\n\n```json\n{\n  \"quote\": <str>,\n  \"amount\": <int>,\n  \"unit\": <str_enum[UNIT]>,\n  \"method\": \"onchain\",\n  \"state\": <str_enum[STATE]>,\n  \"expiry\": <int>,\n  \"request\": <str>,\n  \"fee_options\": [\n    {\n      \"fee_index\": <int>,\n      \"fee_reserve\": <int>,\n      \"estimated_blocks\": <int>\n    }\n  ],\n  \"selected_fee_index\": <int|null>,\n  \"outpoint\": <str|null>\n}\n```\n\nEach item in `fee_options` represents one available fee reserve and confirmation estimate for the same payment. The wallet selects one of these options when executing the melt quote by including the option's `fee_index` value in the melt request. The mint **MUST** return at least one `fee_options` item. The returned `fee_options` are fixed for the lifetime of the quote.\n\nFor each fee option with `fee_index`, `fee_reserve` is the maximum onchain transaction fee the mint may charge for that option, and `estimated_blocks` is the estimated number of blocks until confirmation. `selected_fee_index` is `null` before the quote is executed and is set by the mint to the selected fee option once the wallet executes the quote. The mint expects the wallet to include `Proofs` of _at least_ `total_amount = amount + selected_fee_reserve + input_fee` where `selected_fee_reserve` is the `fee_reserve` from the selected `fee_index` item and `input_fee` is calculated from the keyset's `input_fee_ppk` as described in [NUT-02][02]. If the mint does not claim the full `selected_fee_reserve` as the actual fee, the mint returns the unclaimed amount as `change` to the wallet as described in [NUT-08][08].\n\n`state` is an enum string field with possible values `\"UNPAID\"`, `\"PENDING\"`, `\"PAID\"`:\n\n- `\"UNPAID\"` means that the transaction has not been broadcast yet.\n- `\"PENDING\"` means that the transaction is being processed by the mint but has not reached the required number of confirmations.\n- `\"PAID\"` means that the transaction has been mined and confirmed.\n\n`outpoint` is the transaction ID and output index of the payment in the format `txid:vout`, present once the transaction has been broadcast.\n\n### Melting Tokens\n\nOnchain melt requests are always asynchronous. The mint **MUST** return a `\"PENDING\"` state after validating the melt request and then broadcast the Bitcoin transaction in the background. The wallet **MUST** monitor the quote state through the check quote endpoint until the transaction reaches the required number of confirmations.\n\nFor the `onchain` method, the wallet includes the following specific `PostMeltOnchainRequest` data. The wallet can include an optional `outputs` field in the melt request to receive change for overpaid onchain fees (see [NUT-08][08]):\n\n```json\n{\n  \"quote\": <str>,\n  \"fee_index\": <int>,\n  \"inputs\": <Array[Proof]>,\n  \"outputs\": <Array[BlindedMessage]> // Optional\n}\n```\n\nWhere `fee_index` is the selected fee of the quote's `fee_options`. The mint **MUST** reject a melt request with a `fee_index` that was not returned in the quote. Once `selected_fee_index` is set, the mint **MUST NOT** execute the quote again with a different `fee_index` value.\n\nIf the `outputs` field is included and the mint does not claim the full `selected_fee_reserve` as the actual fee, the mint will respond with a `change` field containing blind signatures for the unclaimed amount (see [NUT-08][08]). The `change` field is omitted if there are no blind signatures to return. Note that because onchain transactions may be batched or have unpredictable costs, the mint is entitled to claim the full `selected_fee_reserve` as the actual fee.\n\n```json\n{\n  \"quote\": <str>,\n  \"amount\": <int>,\n  \"unit\": <str_enum[UNIT]>,\n  \"method\": \"onchain\",\n  \"state\": <str_enum[STATE]>,\n  \"expiry\": <int>,\n  \"request\": <str>,\n  \"fee_options\": [\n    {\n      \"fee_index\": <int>,\n      \"fee_reserve\": <int>,\n      \"estimated_blocks\": <int>\n    }\n  ],\n  \"selected_fee_index\": <int>,\n  \"outpoint\": <str|null>,\n  \"change\": <Array[BlindSignature]> // Optional; present if outputs were included and there's change\n}\n```\n\n### Example\n\n**Melt quote request**:\n\n```bash\ncurl -X POST https://mint.host:3338/v1/melt/quote/onchain -d \\\n'{\"request\": \"bc1q...\", \"unit\": \"sat\", \"amount\": 100000}'\n```\n\n**Melt quote response**:\n\n```json\n{\n  \"quote\": \"019e6d5a-2347-7000-83ad-72a9a83f3105\",\n  \"amount\": 100000,\n  \"unit\": \"sat\",\n  \"state\": \"UNPAID\",\n  \"expiry\": 1701704757,\n  \"request\": \"bc1q...\",\n  \"fee_options\": [\n    {\n      \"fee_reserve\": 5000,\n      \"estimated_blocks\": 1\n    },\n    {\n      \"fee_reserve\": 2000,\n      \"estimated_blocks\": 6\n    },\n    {\n      \"fee_reserve\": 800,\n      \"estimated_blocks\": 144\n    }\n  ],\n  \"selected_fee_index\": null,\n  \"outpoint\": null\n}\n```\n\nCheck quote state:\n\n```bash\ncurl -X GET http://localhost:3338/v1/melt/quote/onchain/019e6d5a-2347-7000-83ad-72a9a83f3105\n```\n\n**Melt request**:\n\n```bash\ncurl -X POST https://mint.host:3338/v1/melt/onchain -d \\\n'{\n  \"quote\": \"019e6d5a-2347-7000-83ad-72a9a83f3105\",\n  \"fee_index\": 1,\n  \"inputs\": [...],\n  \"outputs\": [\n    {\n      \"amount\": 1,\n      \"id\": \"009a1f293253e41e\",\n      \"B_\": \"03327fc4fa333909b70f08759e217ce5c94e6bf1fc2382562f3c560c5580fa69f4\"\n    }\n  ]\n}'\n```\n\n**Pending melt response**:\n\n```json\n{\n  \"quote\": \"019e6d5a-2347-7000-83ad-72a9a83f3105\",\n  \"amount\": 100000,\n  \"unit\": \"sat\",\n  \"state\": \"PENDING\",\n  \"expiry\": 1701704757,\n  \"request\": \"bc1q...\",\n  \"fee_options\": [\n    {\n      \"fee_index\": 0,\n      \"fee_reserve\": 5000,\n      \"estimated_blocks\": 1\n    },\n    {\n      \"fee_index\": 1,\n      \"fee_reserve\": 2000,\n      \"estimated_blocks\": 6\n    },\n    {\n      \"fee_index\": 2,\n      \"fee_reserve\": 800,\n      \"estimated_blocks\": 144\n    }\n  ],\n  \"selected_fee_index\": 1,\n  \"outpoint\": null\n}\n```\n\nThe wallet selects one of the returned `fee_options` by including that option's `fee_index` value in the melt request. Once the quote is executed, quote state checks return the same response shape with `selected_fee_index` set to the selected value and `outpoint` set once the transaction has been broadcast.\n\n**Paid quote state response**:\n\n```json\n{\n  \"quote\": \"019e6d5a-2347-7000-83ad-72a9a83f3105\",\n  \"amount\": 100000,\n  \"unit\": \"sat\",\n  \"state\": \"PAID\",\n  \"expiry\": 1701704757,\n  \"request\": \"bc1q...\",\n  \"fee_options\": [\n    {\n      \"fee_index\": 0,\n      \"fee_reserve\": 5000,\n      \"estimated_blocks\": 1\n    },\n    {\n      \"fee_index\": 1,\n      \"fee_reserve\": 2000,\n      \"estimated_blocks\": 6\n    },\n    {\n      \"fee_index\": 2,\n      \"fee_reserve\": 800,\n      \"estimated_blocks\": 144\n    }\n  ],\n  \"selected_fee_index\": 1,\n  \"outpoint\": \"4d5e6f...:0\",\n  \"change\": [\n    {\n      \"id\": \"009a1f293253e41e\",\n      \"amount\": 1000,\n      \"C_\": \"03c668f551855ddc792e22ea61d32ddfa6a45b1eb659ce66e915bf5127a8657be0\"\n    }\n  ]\n}\n```\n\n### Example `MeltMethodSetting`\n\n```json\n{\n  \"method\": \"onchain\",\n  \"unit\": <str>,\n  \"min_amount\": <int|null>,\n  \"max_amount\": <int|null>\n}\n```\n\n[02]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595cry0t0uzl\n[04]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crg74fknc\n[05]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595cr2dsg4q4\n[08]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595crs4q9zck\n[20]: nostr:naddr1qvzqqqrcvypzp5t83el0jefhfwl2xz9py9tqnfurwmwptqnh5lt9w6q0n4006hpcqqrxuat595erqzryd5a\n","sig":"24e2ddba0e77de12f21731dc1c68a579c9603eb2b38ffef88e8f9c2a8831cfba39b7d1862af10dce221c52db0055606625a4e163b6aefbdfb89170340c29df2a"}