NKIPs

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.

NKIP2
TitleVerifiable Monero Tips on Nostr
StatusDraft
TypeStandards Track
Created2026-10-26
LicenseCC0-1.0
RequiresNIP-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 v tag) 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 v tag is a legacy receipt. It is read with the nkip02/1 rules, except that its amount may not be unique (R13 didn't exist), so it can be confirmed only by tx-proof, never by wallet-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 add v to a guessable amount, which is why the recipient confirms every wallet-match by hand.
  • A receipt has exactly one p tag and at most one e tag. Readers drop receipts with more.
  • For a note tip, p MUST be the author of the note in e. Readers drop note receipts where it isn't.
  • address MUST be a standard address or a subaddress (95 characters). Integrated addresses are not allowed.
  • Readers MUST NOT render tags they don't know.
  • amount is 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.
  • address MUST be the address the tipper actually paid. While a receipt is unconfirmed, it counts as claimed only if address is 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 a monero: URI puts the unique amount in the URI. The amount is what ties a receipt to its transfer without a proof: wallet-match relies 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 address in the proof equals the receipt's address.
  • The proof is the Monero wallet's get_tx_proof(txid, address, message) output, with message set 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 9043 with 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 ran check_tx_proof(txid, address, receipt id, proof), got good: true and at least 10 confirmations, the proof's address equals the receipt's address and belongs to the recipient's wallet, and amount is the received value it returned (converted to decimal XMR).
  • wallet-match: there was no proof, and the receipt has a v tag (legacy receipts can't use this method). The recipient's wallet holds an incoming transfer to address (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's created_at (a receipt follows its send; the 2 hours absorb clock skew; this window is the only rule that reads created_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 by wallet-match automatically: 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-proof confirmation goes to the receipt the proof names (its message); no other receipt can claim that transfer by proof.
  • If several receipts match one transfer by wallet-match, created_at can'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):

  1. Drop receipts that fail the receipt rules, receipts where the tipper is the recipient, and duplicate event ids. For note tips, drop receipts whose p isn't the note author.
  2. A receipt is verified if a confirmation exists that references it, is signed by the receipt's p key (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.
  3. 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.
  4. 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.
  5. 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.
  6. 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).
  7. 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

  1. The viewer resolves the recipient address (NIP-A3) and shows a monero: URI or hands off to its wallet.
  2. The tipper sends. A wallet client sees the send; a client without a wallet asks "Did you send it?".
  3. The tipper's client publishes the receipt (kind 9042).
  4. 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.
  5. The recipient's wallet client unwraps it, runs check_tx_proof, and publishes a confirmation (kind 9044). With no proof, it MAY confirm by wallet-match.
  6. 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 new v.
  • Should viewers drop wallet-match confirmations by default?
  • Test vectors need real get_tx_proof output 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-match depends on them. The earliest-created_at rule for several receipts on one transfer is replaced: created_at is signer-chosen, so a copied receipt could be backdated to win. A proof decides by receipt id; for wallet-match the 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 on created_at. Legacy receipts (no v) carry no unique-amount promise and are confirmed only by tx-proof; new clients MUST add v. The wallet-match window 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, k never exported, one p and at most one e per 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 v tag, 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, monero as 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.

Markdown source · History · Comment