Voucher
Kind 20402 — CMS-signed proof, never published.
Signed by the Coupon Manager Service (CMS) with COUPON_MANAGER_NSEC. Travels in mint/claim JSON. Never published to relays.
{
"kind": 20402,
"pubkey": "<CMS hex>",
"tags": [
["nonce", "hcLPDzERvvHzS4Vn0OLbAQ"],
["p", "<owner hex>"],
["coupon", "<uuid>"],
["phase", "minted"], // or "claimed"
["expiration", "1764633600"] // NIP-40, only if it expires
],
"content": "{…}"
}Payload (v: 1)
{
"v": 1,
"nonce": "hcLPDzERvvHzS4Vn0OLbAQ",
"owner": "<merchant hex>",
"name": "20% de verano",
"description": "No acumulable.",
"coupon": { "type": "percent", "percent": 20 },
"phase": "minted" // | "claimed"
// claimedAt, expiresAt, image — omitted when absent, never null
}Why 20402: ephemeral range. If it leaks to a relay that follows the spec, it is relayed but not stored — right for a bearer nonce.
Verify without calling us
import { verifyEvent } from "nostr-tools/pure"
const cmsPubkey = announcement.tags.find((t) => t[0] === "p")?.[1]
if (voucher.pubkey !== cmsPubkey) throw new Error("different issuer")
if (!verifyEvent(voucher)) throw new Error("invalid signature")
const payload = JSON.parse(voucher.content)
if (payload.nonce !== response.nonce) throw new Error("different nonce")
if (payload.owner !== expectedOwnerHex) throw new Error("different merchant")TLS proves the response came from the host. The CMS signature proves it came from the service the merchant named.