NKIP-0002 · Draft
Verifiable Monero Tips on Nostr
Abstract. A tipper publishes a receipt for a Monero tip. A payment proof goes only to the recipient, encrypted, and the recipient publishes a confirmation, so clients can show verified tip totals without any transaction id becoming public.
| NKIP | 2 |
|---|---|
| Title | Verifiable Monero Tips on Nostr |
| Status | Draft |
| Type | Standards Track |
| Created | 2026-10-26 |
| License | CC0-1.0 |
| Requires | NIP-01, NIP-44, NIP-59, NIP-A3 |
Files: events.schema.json · proof.schema.json
Contents
Motivation
Monero tips on Nostr are already shown from receipts the tipper signs (kind 9042). Any key can publish one for any amount, so totals built from receipts alone can be inflated at no cost. A public transaction id would make them checkable, but tying a tx hash to a Nostr identity links sender and transaction, which is what Monero hides.
Only the recipient can check a payment without that link being public: their wallet already sees the incoming transfer. This NKIP lets the recipient confirm a tip, publicly and without the txid.
Goals:
- G1 A client can tell a tip the recipient confirmed apart from one that is only claimed.
- G2 No txid, tx key or payment proof is ever public.
- G3 Clients that hold no wallet (browser extensions, web clients) can still tip, read totals and show which tips are verified.
- G4 Kind 9042 receipts published before this NKIP (no
vtag) stay valid; they count as claimed until confirmed.
Non-goals:
- Proving a tip to anyone other than the recipient.
- Lightning (NIP-57 zaps, kind 9735).
- Sending funds. Clients hand off to a wallet; nothing here moves money.
Roles
| Role | Who | Job |
|---|---|---|
| Tipper | The person tipping | Sends XMR from their wallet; signs the receipt |
| Recipient | The author being tipped | Publishes a NIP-A3 address; confirms tips that arrived |
| Wallet client | A Nostr client with a Monero wallet | Produces the payment proof and checks incoming proofs automatically |
| Viewer | Any client showing tips (e.g. a browser extension or web client) | Counts receipts and confirmations; needs no wallet |
Specification
The key words MUST, MUST NOT, SHOULD and MAY are used as in RFC 2119. Kinds 9042, 9043 and 9044 are provisional; 9042 is already in use by receipts published before this NKIP. The authors will request these kinds upstream (nostr-protocol/nips). If different kinds are assigned, a later version moves to them and readers accept both during a transition of at least 12 months.
Recipient address
The recipient's tip address is the first valid ["payto","monero"|"xmr",<address>]
tag of their newest kind 10133 event (NIP-A3). monero is the canonical NIP-A3 type; xmr is accepted as an alias. A kind 0 monero, xmr or
monero_address field MAY be used when there is no kind 10133. Clients MUST
validate the address (mainnet, checksum) before showing it.
Receipt (kind 9042)
Signed by the tipper after the send.
| Field | Value |
|---|---|
kind |
9042 |
content |
"". Legacy receipts may carry text; readers ignore it |
tags |
["e", <note id>] (omit for a profile tip), ["p", <recipient pubkey>], ["amount", <decimal XMR>], ["unit", "xmr"], ["address", <address paid>], ["client", <name>], optionally ["v", "nkip02/1"] |
Rules:
- A receipt without a
vtag is a legacy receipt. It is read with thenkip02/1rules, except that its amount may not be unique (R13 didn't exist), so it can be confirmed only bytx-proof, never bywallet-match. New clients MUST add the tag; the tag is their promise that the amount is unique. It is only a promise: a dishonest tipper can addvto a guessable amount, which is why the recipient confirms everywallet-matchby hand. - A receipt has exactly one
ptag and at most oneetag. Readers drop receipts with more. - For a note tip,
pMUST be the author of the note ine. Readers drop note receipts where it isn't. addressMUST be a standard address or a subaddress (95 characters). Integrated addresses are not allowed.- Readers MUST NOT render tags they don't know.
amountis a decimal XMR string with at most 12 fractional digits (e.g."0.001000437215"). Readers convert to piconero with fixed-point arithmetic, never floating point.- A receipt MUST NOT carry a
txid,tx, tx key or proof tag. Readers MUST ignore any such tag and SHOULD NOT display it. addressMUST be the address the tipper actually paid. While a receipt is unconfirmed, it counts as claimed only ifaddressis one of the recipient's current NIP-A3 or kind 0 addresses (see Counting for confirmed receipts).- A tipping client MUST make the amount unique by adding 1–999,999 random piconero (at most 0.000001 XMR) to the amount the user chose (e.g.
0.001000437215), and publish the exact amount sent. A client that hands off through amonero:URI puts the unique amount in the URI. The amount is what ties a receipt to its transfer without a proof:wallet-matchrelies on nobody else being able to guess it before the receipt is public. - A wallet client SHOULD publish the receipt only after its wallet reports the send. A client without a wallet MUST ask the user to confirm the send before publishing, and MUST NOT publish without an amount.
Payment proof (kind 9043, gift-wrapped)
The tipper's client sends the recipient a proof the recipient's wallet can check.
- The tipper's client MUST check that
addressin the proof equals the receipt'saddress. - The proof is the Monero wallet's
get_tx_proof(txid, address, message)output, withmessageset to the receipt's event id (lowercase hex). That ties the proof to one receipt, so it can't be reused for another. - The client builds an unsigned rumor (NIP-59) of kind
9043with tags["p", <recipient>],["e", <receipt id>], and content the JSON object below. It seals and gift-wraps it to the recipient per NIP-59 (NIP-44 encryption) and publishes the wrap to the recipient's NIP-65 read relays or kind 10050 DM relays. - The proof MUST NOT be sent any other way, and MUST NOT be published unwrapped.
{
"v": "nkip02/1",
"receipt": "<receipt event id, hex>",
"txid": "<64 hex>",
"address": "<address paid, same as the receipt>",
"proof": "OutProofV2…"
}
Schema: nkip-0002/proof.schema.json.
get_tx_proof for an outgoing transfer needs the tx secret key, which the wallet stores when it sends. A wallet restored from seed has lost it for earlier sends and cannot make the proof; such tips can only be confirmed by wallet-match.
A wallet client SHOULD send the proof automatically. A client without a wallet MAY offer a field to paste the txid and proof from the user's wallet; it MUST NOT ask for the tx private key.
Confirmation (kind 9044)
Signed by the recipient (the receipt's p key) once the tip has been checked.
| Field | Value |
|---|---|
kind |
9044 |
content |
"" |
tags |
["e", <receipt id>], ["p", <tipper pubkey>], ["amount", <decimal XMR confirmed>], ["method", "tx-proof" \| "wallet-match"], ["t", <transfer tag>], ["v", "nkip02/1"] |
The transfer tag lets viewers see that two confirmations point at the same
transfer without learning the txid:
t = lowercase hex of HMAC-SHA256(k, txid), where txid is the 32 raw bytes and
k = HKDF-SHA256(ikm = the wallet's private view key (32 raw bytes), salt = "nkip02", info = "transfer tag v1", L = 32). Salt and info are UTF-8 bytes with no terminator. k is derived inside the wallet and MUST NOT be exported. Wallet setups that never expose the view key to the confirming client cannot derive k, and therefore cannot confirm.
Confirming always needs the recipient's wallet (to check the proof or the
incoming transfer), so every confirming client can derive k, including clients
that sign Nostr events through NIP-07 or NIP-46. Every device with that wallet
derives the same k, so the same transfer always gets the same tag. Since txids
are public, a plain hash would reveal the txid to anyone who hashes the chain;
the key prevents that. HKDF is one-way, so a leaked k never reveals the view
key, but it does let the holder map this recipient's tags to txids.
Methods:
tx-proof: the recipient's wallet rancheck_tx_proof(txid, address, receipt id, proof), gotgood: trueand at least 10 confirmations, the proof'saddressequals the receipt'saddressand belongs to the recipient's wallet, andamountis thereceivedvalue it returned (converted to decimal XMR).wallet-match: there was no proof, and the receipt has avtag (legacy receipts can't use this method). The recipient's wallet holds an incoming transfer toaddress(or one of its subaddresses) with exactly the receipt's amount, included in a block whose timestamp is no later than 2 hours after the receipt'screated_at(a receipt follows its send; the 2 hours absorb clock skew; this window is the only rule that readscreated_at, and a wrong timestamp can only narrow what the recipient is shown) and no earlier than 30 days before it, with at least 10 confirmations, and that transfer is not used for any other confirmation. This is weaker; clients MAY show it differently. A recipient's client MUST NOT confirm bywallet-matchautomatically: the recipient confirms each match by hand.
Rules:
- A confirmation MUST NOT contain the txid, tx key or proof.
- Per recipient, one transfer confirms at most one receipt. A transaction paying two recipients can be confirmed once by each. The recipient's client enforces this with the transfer tag, checking its own earlier confirmations on relays as well as local state, so a second device or lost state can't confirm the same transfer twice.
- A
tx-proofconfirmation goes to the receipt the proof names (itsmessage); no other receipt can claim that transfer by proof. - If several receipts match one transfer by
wallet-match,created_atcan't decide between them: the signer sets it, so a copy of a public receipt can be backdated. The recipient's client MUST show all matching receipts and MUST NOT preselect one; the recipient confirms one of them or none. The others stay claimed. - This rule binds honest clients. A dishonest recipient can still publish confirmations for tips that never happened; a confirmation is only ever the recipient's own word.
- While a transfer waits for its 10 confirmations (about 20 minutes), clients SHOULD show the tip as "verifying", not "unverified".
- If the recipient changes Nostr keys, receipts addressed to the old key can only be confirmed with the old key; otherwise they stay claimed.
- A recipient MAY withdraw a confirmation with a NIP-09 deletion (kind 5). Readers MUST then treat the receipt as claimed, and the transfer becomes free to confirm another receipt.
- A confirmation for an unknown receipt id is ignored until the receipt is seen.
Counting
For a note (receipts with its e tag) or a profile (receipts with no e tag and its p):
- Drop receipts that fail the receipt rules, receipts where the tipper is the recipient, and duplicate event ids. For note tips, drop receipts whose
pisn't the note author. - A receipt is verified if a confirmation exists that references it, is signed by the receipt's
pkey (for note tips, the note author) and hasn't been deleted. Its verified amount is the lower of the receipt amount and the confirmed amount. - Viewers dedupe confirmations by (signer, transfer tag): when several share a tag, only the one whose receipt has the lowest event id (lexicographic hex) counts, and the others' receipts stay claimed. This only happens when a recipient confirmed one transfer twice. The rule doesn't use
created_at, which the signer sets; it just makes every viewer pick the same one. - A verified receipt's address is vouched for by the recipient's confirmation, so it stays verified after the recipient changes its published address. The published-address check (R10) applies only to claimed receipts.
- Every other remaining receipt is claimed. For claimed totals, count at most one receipt per tipper per note or profile, the one with the smallest amount: claims cost nothing, so publishing more receipts must never raise a tipper's claimed figure. The trade-off is deliberate: a tipper's own extra small receipt can hide their larger claim, which only affects that tipper. Verified receipts are not capped; each is a distinct transfer (step 3), so a tipper who tips again shows in full once the recipient confirms.
- Clients SHOULD show the number of distinct tippers next to the verified total, and MAY weight or filter tippers by the viewer's web of trust (e.g. follows of follows).
- Clients MUST label the two totals separately (for example
ɱ 0.004 ✓ +0.002 claimed) and MUST NOT add them into a single number without saying so.
Clients SHOULD bound what they fetch, for example at most 500 receipts and 500 confirmations per note or profile, newest first, so a flood of fake events can't stall them. When a confirmation points at a receipt beyond the cap, clients fetch that receipt by id.
Relay filters:
- Receipts for notes:
{"kinds":[9042],"#e":[<note ids>]} - Confirmations:
{"kinds":[9044],"#e":[<receipt ids>],"authors":[<recipient>]}, where the recipient for a note tip is the note author - Tips to me:
{"kinds":[9042],"#p":[<my pubkey>]}
Flow
- The viewer resolves the recipient address (NIP-A3) and shows a
monero:URI or hands off to its wallet. - The tipper sends. A wallet client sees the send; a client without a wallet asks "Did you send it?".
- The tipper's client publishes the receipt (kind 9042).
- A wallet client creates the proof with
message= receipt id and sends it gift-wrapped (kind 9043). A client without a wallet MAY forward a pasted proof the same way. - The recipient's wallet client unwraps it, runs
check_tx_proof, and publishes a confirmation (kind 9044). With no proof, it MAY confirm bywallet-match. - Viewers show the verified and claimed totals.
Requirements
| ID | Level | Goal | Requirement | Acceptance check |
|---|---|---|---|---|
| R1 | MUST | G2 | Receipts and confirmations carry no txid, tx key or proof | Test vectors; reader rejects/ignores such tags |
| R2 | MUST | G2 | Proofs travel only as NIP-59 gift wraps to the recipient | Code review |
| R3 | MUST | G1 | get_tx_proof message is the receipt event id |
Test vectors |
| R4 | MUST | G1 | Confirmations count only when signed by the receipt's p key |
Test vectors |
| R5 | MUST | G1 | Verified, verifying and claimed tips are shown as separate states | UI review |
| R6 | MUST | G1 | Confirmations carry the transfer tag; per recipient, one transfer confirms at most one receipt (enforced by honest clients); viewers dedupe by (signer, tag), keeping the confirmation whose receipt has the lowest event id | Test vectors with two receipts on one transfer |
| R7 | MUST | G4 | Receipts without a confirmation are counted as claimed, not dropped | Test vectors include a legacy receipt (no v tag, non-empty content) |
| R8 | MUST | G3 | A client without a wallet asks the user to confirm the send before publishing a receipt | Extension test |
| R9 | SHOULD | G1 | Wallet clients send proofs and confirm incoming tips automatically, with an opt-out | Manual |
| R10 | MUST | G1 | Claimed receipts whose address isn't the recipient's current NIP-A3 or kind 0 address don't count |
Test vectors |
| R12 | MUST | G1 | Note-tip receipts count only when p is the note author, and confirmations only when signed by that author |
Test vectors |
| R13 | MUST | G1 | Tipping clients add 1–999,999 random piconero to the chosen amount, publish the exact amount sent (also in a monero: URI handoff) and add the v tag; receipts without v are confirmed only by tx-proof |
Code review; test vector: a legacy receipt is never matched by wallet-match |
| R11 | MUST before leaving Draft | — | This NKIP ships schemas, test vectors (including a legacy receipt and two receipts on one transfer) and a version field | CI on this repo |
Security considerations
| Threat | Likelihood | Control |
|---|---|---|
| Fake receipts inflating totals | High | Counted as claimed only, at most one per tipper per note (the smallest); only confirmations make a tip verified |
| Proof reused for a second receipt | Medium | get_tx_proof message is the receipt id; one transfer confirms one receipt |
| Wash tipping: the recipient tips themselves from throwaway keys and confirms | High | Costs only network fees, since the funds come back. Viewers show distinct tippers and weight tippers by web of trust; a verified total means "the recipient says it received this", not popularity |
| Third party attaches self-confirmed tips to someone else's note | High | R12: a note receipt counts only when p is the note author, and only the author can confirm it |
| Recipient confirms one transfer for many receipts | Medium | Transfer tag plus viewer dedupe. A recipient who wants to lie can still confirm tips that never happened; a confirmation is only ever the recipient's own word |
| Recipient confirms tips that never arrived | Low | It's the recipient's own reputation; a confirmation only says "I got it". Viewers can weight by web of trust |
| Credit stealing: a receipt claims someone else's anonymous transfer | Medium | tx-proof binds the proof to the receipt id, so a copy can't be proven. Without a proof, the unique amount is unknown until the real receipt is public; a copy made after that (even backdated) is shown to the recipient next to the original, and wallet-match is manual only |
| Recipient refuses to confirm a real tip | Medium | The tip stays claimed; no funds are affected. The tipper still has the proof |
| txid leaked through the proof message | Low | Gift wrap hides sender, recipient and kind from relays; only the recipient decrypts |
| Recipient links the tipper's Nostr key to a transaction | Certain (by design) | The recipient learns which txid came from which key. Tippers who don't want that skip the proof; the tip then stays claimed or is matched by hand |
Timing: receipt created_at close to the transaction on chain |
Medium | Ring signatures hide the sender's input. Tippers who care MAY delay publishing the receipt by up to 30 days; it can still be confirmed by tx-proof, or by wallet-match within its window |
wallet-match confirms the wrong transfer |
Medium | Exact unique amount (random piconero, MUST); the transfer precedes the receipt by at most 30 days; the recipient confirms each match by hand; labelled as the weaker method |
| Receipt names an address the recipient doesn't control | Medium | R10: the address must be the recipient's published address |
| Confirmation from a different key | Low | R4: only the receipt's p key counts |
Open questions
- Register kinds 9042, 9043 and 9044 upstream (nostr-protocol/nips), or pick replacements. 9042 is already in use.
- [x] Amount unit: decimal XMR in
nkip02/1. Existing receipts use it, and fixed-point parsing makes it exact. A switch to piconero would need a newv. - Should viewers drop
wallet-matchconfirmations by default? - Test vectors need real
get_tx_proofoutput from a stagenet wallet. Test mode: a reader in test mode accepts stagenet addresses instead of mainnet ones; nothing else changes. Production readers MUST NOT accept stagenet addresses.
Changelog
- 2026-10-27: Vendor-neutral wording (no client named in the rules or vectors). Unique random-piconero amounts are now MUST, since
wallet-matchdepends on them. The earliest-created_atrule for several receipts on one transfer is replaced:created_atis signer-chosen, so a copied receipt could be backdated to win. A proof decides by receipt id; forwallet-matchthe recipient sees every matching receipt and picks one or none. Viewer dedupe of a doubly confirmed transfer now uses the lowest receipt id, and the claimed cap keeps each tipper's smallest receipt, so no counting rule depends oncreated_at. Legacy receipts (nov) carry no unique-amount promise and are confirmed only bytx-proof; new clients MUST addv. Thewallet-matchwindow now runs from 30 days before the receipt to 2 hours after it, so a tipper who delays the receipt for privacy can still be matched. The claimed-total cap is explained (verified receipts are not capped). Amount unit settled: decimal XMR in v1. - 2026-10-26: Review round 3 (passed 8.3), then: UTF-8 HKDF inputs,
knever exported, onepand at most oneeper receipt, transfer released on withdrawal, verifying state in R5, R11 as a Draft exit gate. - 2026-10-26: Review round 3: transfer-tag key from the wallet via HKDF with byte-level encoding, portable schema regex, R6 scoped per recipient and to honest clients, aligned tie-breaks, block-time window, fetch bounds, "verifying" state, key-rotation note.
- 2026-10-26: Review round 2: note-author binding (R12), transfer tag and viewer dedupe (R6), 10 confirmations, explicit wallet-match window, receipt
vtag, no integrated addresses, address history via confirmations, kind transition rule, stagenet test mode. - 2026-10-26: Review fixes: manual-only
wallet-match, random piconero in amounts, tie-break rule, wash-tipping and credit-stealing threats, restored-wallet note,moneroas canonical type, distinct-tipper count. - 2026-10-26: Draft. Receipt rules from the existing kind 9042 receipts, gift-wrapped payment proof (9043), recipient confirmation (9044), counting rules.