{"id":"eb286e5ec4d702b459af1611a9575ebb19beade43081c79ecc92f56e6a094f0d","pubkey":"f6150173b5d6f079b43540d84a8a95d50cf01a48c9d6037984e3d9600d5522af","created_at":1784763123,"kind":30817,"tags":[["d","badge-nip58-requests-and-denials"],["title","Badge NIP58 - Requests and Denials"],["alt","Nostr Implementation Possibility: Badge NIP58 - Requests and Denials"],["k","30058","Request"],["k","30059","Denied"],["client","NostrHub"]],"content":"*An extension to NIP-58 for requestable badges, with private notifications for issuers.*\n\n> Status: Draft\n> Depends on: [NIP-58](https://github.com/nostr-protocol/nips/blob/master/58.md), [NIP-09](https://github.com/nostr-protocol/nips/blob/master/09.md), [NIP-17](https://github.com/nostr-protocol/nips/blob/master/17.md), [NIP-59](https://github.com/nostr-protocol/nips/blob/master/59.md)\n> Reference PR: [nostr-protocol/nips#2204](https://github.com/nostr-protocol/nips/pull/2204)\n\n---\n\n## What this is\n\nNIP-58 gives us badges: an issuer defines a badge, awards it to someone, and that person can display it on their profile. It is a one-way flow. The issuer decides who gets a badge, and there is no standard way for a user to raise their hand and say *\"I earned this, please consider me.\"*\n\nThis extension closes that gap. It adds two new events so that badges can be **requested**, and requests can be **denied** with a reason. Nothing about existing NIP-58 changes ; definition, award and Profile Badges events all work exactly as before. Clients that do not understand the new kinds simply ignore them.\n\n> **Note on the Profile Badges event.** Current NIP-58 uses the replaceable kind `10008` for Profile Badges. The older addressable kind `30008` with `d=profile_badges` is deprecated, and clients should treat those legacy events as equivalent. This extension follows that lead: **write new, read both.** See [Displaying awarded badges](#displaying-awarded-badges-profile-badges) below.\n\nOn top of the on-relay events, this document also describes how an issuer is **notified** when a request arrives:\n\n1. A public **Badge Request** event tagged to the issuer, which their client can surface in a dedicated **badge inbox**.\n2. An optional **private direct message** to the issuer, sealed with NIP-17 + NIP-59, so the notification also lands in a normal DM client the issuer already checks.\n\n---\n\n## The events at a glance\n\n| Kind    | Name          | Published by | Class       | Purpose                                          |\n| ------- | ------------- | ------------ | ----------- | ------------------------------------------------ |\n| `30058` | Badge Request | Requester    | Addressable | Ask an issuer to award an existing badge         |\n| `30059` | Badge Denial  | Issuer       | Addressable | Formally decline a request, with an optional note |\n\nBoth are addressable (parameterized replaceable) events, so a later event with the same `d` tag from the same author supersedes the earlier one. That gives us edit and re-submit semantics for free.\n\n---\n\n## Badge Request (kind `30058`)\n\nPublished by the **user** who wants the badge. It points at an existing Badge Definition (kind `30009`) and at the issuer, and it may carry evidence.\n\n### Tags\n\n| Tag     | Required | Value                                                        |\n| ------- | -------- | ----------------------------------------------------------- |\n| `d`     | yes      | The badge's `a` coordinate, e.g. `30009:alice:bravery`. This makes the request addressable per badge. |\n| `a`     | yes      | Reference to the Badge Definition, with an optional relay hint. |\n| `p`     | yes      | The issuer's pubkey. This is what lets the issuer's client find requests addressed to them. |\n| `proof` | no       | One or more proof tags supporting the request. Each value may be a URL, free text, or a Nostr event id. |\n\nThe `content` field is a free-form message from the requester to the issuer.\n\n### Example\n\nBob asks Alice for her `bravery` badge and links a news article as evidence:\n\n```jsonc\n{\n  \"kind\": 30058,\n  \"pubkey\": \"bob\",\n  \"content\": \"I helped rescue the trapped hikers last month!\",\n  \"tags\": [\n    [\"d\", \"30009:alice:bravery\"],\n    [\"a\", \"30009:alice:bravery\", \"wss://relay\"],\n    [\"p\", \"alice\"],\n    [\"proof\", \"https://news.example/hiker-rescue-article\"]\n  ]\n  // ...id, sig, created_at\n}\n```\n\nBecause the `d` tag is the badge coordinate, Bob has at most one active request per badge. If he wants to strengthen his case, he republishes kind `30058` with the same `d` and better `proof` tags ; the new event replaces the old one.\n\n---\n\n## Badge Denial (kind `30059`)\n\nPublished by the **issuer** to formally decline a specific request. Denials are soft by design: they explain what is missing and leave the door open for a stronger re-submission.\n\n### Tags\n\n| Tag | Required | Value                                                     |\n| --- | -------- | -------------------------------------------------------- |\n| `d` | yes      | The request event's id. One denial addresses one request. |\n| `a` | yes      | Reference to the Badge Definition, with an optional relay hint. |\n| `e` | yes      | The request event being denied, with an optional relay hint. |\n| `p` | yes      | The requester's pubkey, so the denial reaches their client. |\n\nThe `content` field carries the reason, which the requester can act on.\n\n### Example\n\nAlice declines Bob's request and tells him what she needs:\n\n```jsonc\n{\n  \"kind\": 30059,\n  \"pubkey\": \"alice\",\n  \"content\": \"Please provide photo evidence or witness confirmation.\",\n  \"tags\": [\n    [\"d\", \"<bob's request event id>\"],\n    [\"a\", \"30009:alice:bravery\", \"wss://relay\"],\n    [\"e\", \"<bob's request event id>\", \"wss://relay\"],\n    [\"p\", \"bob\"]\n  ]\n  // ...id, sig, created_at\n}\n```\n\nA denial is never final. Bob can submit a fresh request with the missing evidence, which produces a new request event that the old denial no longer refers to.\n\n---\n\n## Request lifecycle\n\nA request that exists on relays resolves to one of **three** states. Clients evaluate them **in priority order** and stop at the first match:\n\n| Priority | State         | Condition                                                     |\n| -------- | ------------- | ------------------------------------------------------------- |\n| 1        | **Fulfilled** | A Badge Award (kind `8`) exists for this badge and user. The user can then accept it into their Profile Badges (see below). |\n| 2        | **Denied**    | A Badge Denial (kind `30059`) exists for this request.        |\n| 3        | **Pending**   | A request exists and none of the above apply.                 |\n\nFulfillment wins over everything. If an issuer awards the badge, that is the answer regardless of any earlier denial.\n\n### Withdrawal and revocation use NIP-09\n\nThere is no fourth \"withdrawn\" state and no status tag. State changes are expressed by deleting the underlying event with standard NIP-09 deletion (kind `5`):\n\n- **Withdraw a request:** the requester deletes their own kind `30058` event. The request no longer exists, so it drops out of the states above entirely.\n- **Revoke a denial:** because a Badge Denial is an addressable event, the issuer can delete it (NIP-09) or supersede it, for example after deciding to award the badge after all. A request whose denial is gone falls back to Pending (or Fulfilled, once the award is published).\n\nThis keeps state changes in one well-understood mechanism instead of a bespoke lifecycle. The core NIP-58 proposal specifies withdrawal via NIP-09 explicitly; denial revocation follows from the same deletion semantics.\n\n---\n\n## Displaying awarded badges (Profile Badges)\n\nWhen a request is **Fulfilled**, the issuer has published a Badge Award (kind `8`). For that badge to actually show on the requester's profile, the requester accepts it into their **Profile Badges** event. Current NIP-58 changed which kind carries that list, and this extension aligns with the new standard:\n\n| Kind    | Class       | Status                                                        |\n| ------- | ----------- | ------------------------------------------------------------ |\n| `10008` | Replaceable | **Current.** The Profile Badges event. Write here.           |\n| `30008` | Addressable | **Deprecated** (`d=profile_badges`). Read for compatibility only. |\n\nNIP-58 says clients should treat the legacy `30008` events as equivalent to `10008`. The practical rule, and the one BadgeBox follows, is **write new, read both**:\n\n- **Writing (accepting a badge):** publish the user's Profile Badges only as kind `10008`. Do not create new `30008` events.\n- **Reading (rendering a profile):** load the user's Profile Badges from **both** kind `10008` and the legacy kind `30008` with `d=profile_badges`, and treat them as the same list. This keeps badges that older clients wrote still visible.\n\nThis only concerns how an awarded badge is displayed. It does not change the request (`30058`) or denial (`30059`) events, which are independent of the Profile Badges kind.\n\n---\n\n## Private notifications for the issuer (NIP-17 + NIP-59)\n\n> **Scope.** This section is an **implementation convention**, not part of the core request/denial events. The NIP-58 proposal does not require a DM; it only recommends that issuer clients provide an inbox. BadgeBox and Lotus layer the DM on top so issuers are notified through a channel they already watch. A client can implement the request/denial spec fully without it.\n\nThe public request event is enough for a client to build a badge inbox by subscribing to kind `30058` filtered on `#p = <issuer>`. But an issuer who is not actively watching their inbox can miss it. So BadgeBox additionally sends the issuer a **private direct message** the moment a request is published.\n\nThe DM uses standard sealed messaging exactly as defined by NIP-17 and NIP-59:\n\n- **NIP-17** defines the private direct message as a kind `14` chat message (an unsigned rumor), `p`-tagged to the receiver.\n- **NIP-59** wraps it in two layers: the rumor is sealed in a kind `13` event (encrypted to the receiver, no `p` tag), and the seal is gift-wrapped in a kind `1059` event that carries the routing tags. Both layers use NIP-44 encryption.\n\nThe flow when Bob requests Alice's badge:\n\n1. Bob's client publishes the public Badge Request (kind `30058`) as usual.\n2. Bob's client also builds a NIP-17 chat message (kind `14`) to Alice, seals it (kind `13`), and gift-wraps it (kind `1059`) per NIP-59.\n3. The gift wrap is published to Alice's DM relays (her NIP-17 inbox relays, per kind `10050`).\n4. Alice unwraps it in any NIP-17 capable client and sees a human-readable heads-up (see the message pattern below).\n\n### What the message should contain\n\nThe recommended pattern is to mirror the full request in the DM so the issuer can judge it without leaving their messaging client. That means **the request details and the proof, when proof was supplied**. Concretely:\n\n- **Who and what:** the requester (npub or resolved profile name) and the badge being requested (name plus its `a` coordinate).\n- **The message:** the request's `content`, if present.\n- **The proof:** every `proof` tag, **rendered by type** rather than dumped as raw strings:\n  - a URL becomes a clickable link (and, where the client supports previews, a link card),\n  - a `nostr:` event id or `nevent`/`note` reference becomes a quoted or linked event,\n  - free text is shown as a short quote.\n- **An action link:** a deep link back to the request in NostrHub / BadgeBox so Alice can award or deny in one tap.\n\nBecause NIP-17 messages are plain text, do the rendering with conventions the receiving client already understands: real URLs (auto-linked), `nostr:` URIs for events and profiles (NIP-21), and simple line breaks. Do not invent custom markup that only your client can read.\n\nA well-formed notification, as delivered by BadgeBox, reads like:\n\n> **New badge request: Bravery**\n> From nostr:npub1bob... (Bob)\n> Badge: `30009:alice:bravery`\n>\n> \"I helped rescue the trapped hikers last month!\"\n>\n> Proof:\n> - https://news.example/hiker-rescue-article\n> - nostr:nevent1... (linked witness note)\n>\n> Review it: https://badgebox.rinbal.de/requests/...\n\nIf a request carries **no** proof tags, omit the Proof block entirely rather than showing an empty heading.\n\nThe DM is a **notification, not the source of truth**. The authoritative record is always the public kind `30058` event, including its proof tags. If a client only reads the public event, nothing is lost ; the DM just makes sure the issuer hears about it promptly, privately, and with everything needed to decide.\n\n> **Note on privacy:** because the Badge Request itself is a public event, the request is not confidential. The DM does not add secrecy to the request ; it adds reliable, direct delivery to the issuer's regular messaging surface.\n\n---\n\n## Recommendations for clients\n\n**For issuer clients**\n\n- Provide a **badge inbox**: subscribe to kind `30058` with `#p = <issuer pubkey>`, group by badge, and show each request with its message and proof tags.\n- Offer one-tap **Award** (publish kind `8`) and **Deny** (publish kind `30059`) actions from the inbox.\n- Optionally send the NIP-17 notification described above so requests are not missed.\n\n**For requester clients**\n\n- On any badge the user does not yet hold, show a **\"Request Badge\"** action that publishes kind `30058`.\n- Let users attach one or more proof tags and a short message.\n- Surface the resolved state (Pending, Denied, Fulfilled) and, on denial, show the issuer's reason and a **Re-submit** path. A withdrawn request is simply deleted, so it leaves the list.\n\n**For every client**\n\n- **Rate-limit** request submissions per user and per badge to keep issuers from being spammed.\n- Treat unknown kinds gracefully. A client that does not implement this extension keeps working with plain NIP-58.\n\n---\n\n## Backward compatibility\n\nThis is purely additive. It introduces two new event kinds and reuses NIP-09 for state changes. Existing NIP-58 definition, award and profile events are untouched, and clients that have never heard of `30058` or `30059` simply ignore them. Adopting the extension is opt-in on both the issuer and requester sides.\n\n---\n\n## Summary\n\n| Concern              | Mechanism                                             |\n| -------------------- | ----------------------------------------------------- |\n| Request a badge      | Kind `30058`, addressable, with `proof` tags          |\n| Deny a request       | Kind `30059`, addressable, with a reason              |\n| Withdraw / revoke    | NIP-09 deletion (kind `5`), no status tag              |\n| Issuer awareness     | Badge inbox on `#p` filter; optional NIP-17 + NIP-59 DM |\n| Resolve state        | Fulfilled > Denied > Pending (three states)           |\n| Display awarded badge | Profile Badges: write kind `10008`, read `10008` + legacy `30008` |\n| Compatibility        | Additive, unknown kinds ignored                       |\n\n---\n\n## Specification and status\n\nThis document describes an open, in-progress extension to NIP-58. The canonical proposal, its discussion, and the latest wording live in the Nostr NIPs repository:\n\n- **Open spec (NIP proposal):** [nostr-protocol/nips#2204 - \"NIP58 - Badge Event 'Request' and 'Denial'\"](https://github.com/nostr-protocol/nips/pull/2204)\n\nThe PR is open and awaiting more implementers before merge. Feedback, review and additional implementations are welcome there.\n\n## Implementations\n\nThe extension is already implemented and running in production, which is what this proposal needs to move forward:\n\n- **BadgeBox** - [badgebox.rinbal.de](https://badgebox.rinbal.de)\n- **Lotus** - [lotus.mybuho.de](https://lotus.mybuho.de)\n\nBoth publish and read the request (`30058`) and denial (`30059`) events, surface incoming requests in an issuer inbox, send the NIP-17 + NIP-59 private notification, and follow the \"write `10008`, read `10008` + legacy `30008`\" rule for Profile Badges.","sig":"743671cbb185892e1aef367c2f0d8fe89f24f018b05f3c4c3caee6f2833b4593ca333de7e193726d055774844606aee67ed9af6146277471c5bf8560eedfbbfa"}