# NIP: Custom Event Kinds ## Event Kinds Overview ### Ditto Kinds | Kind | Name | Description | |-------|----------------------|-------------------------------------------------------| | 16769 | Profile Tabs | The user's custom profile page tabs (one per user) | ### Agora Kinds | Kind | Name | Description | |-------|----------------------------|----------------------------------------------------------------| | 33863 | Campaign | Self-authored fundraising campaign with a single Bitcoin wallet endpoint (`bc1...` or `sp1...`) | | 30385 | Community Stats Snapshot | Pre-computed per-country / global community leaderboards | | 36639 | Pledge | Donor pledge for concrete submissions, stored as sats | ### Agora Protocols | Protocol | Composed Kinds | Description | |--------------------------|-----------------------------------------|-----------------------------------------------------------------| | Flat Communities | 34550, 30009, 8, 1111, 1984 | One-level badge membership with explicit moderators (NIP-72 ext) | | Community Chat | 34550, 1311 | Realtime member chat scoped to a NIP-72 community | | Campaign Moderation | 33863, 1985, 39089 | Homepage curation (approved / hidden / featured axes) via moderator-signed labels in the `agora.moderation` namespace, gated by a follow-pack moderator roster | ### Agora Content Marker Every event Agora publishes that represents a first-class Agora object carries the single-letter tag `["t", "agora"]`. This marker enables the Agora activity feed to filter strictly server-side via the relay-indexed `#t` filter (multi-letter tags like the NIP-89 `client` tag are not indexed by relays and are therefore unsuitable for this purpose). #### Tagged kinds | Kind | Object | Where tagged | |-------|---------------------|---------------------------------------------------------------| | 1 | Note (top-level, reply, quote) | `ComposeBox` default for top-level kind 1 publishes | | 1111 | NIP-22 comment | `usePostComment` (all comments authored in Agora) | | 8333 | Onchain zap | `useOnchainZap`, `useDonateCampaign`, `SendBitcoinDialog` | | 9041 | Zap goal | `CreateGoalDialog` | | 33863 | Campaign | `CreateCampaignPage` | | 31922 | Date calendar event | `CreateEventPage`, `CreateCommunityEventDialog` | | 31923 | Time calendar event | `CreateEventPage`, `CreateCommunityEventDialog` | | 34550 | Community | `CreateCommunityPage` | | 36639 | Pledge | `CreateActionPage` | The tag is added at publish time via the `withAgoraTag` helper in `src/lib/agoraNoteTags.ts`, which dedupes against any user-supplied `t:agora` tag. #### Untagged kinds (intentional) Reactions, reposts, follow lists, profile metadata, lists, settings, badges, vanish requests, encrypted DMs, and live chat are user-state or response events rather than first-class Agora content. Tagging them would pollute `#agora` hashtag surfaces without adding value to the activity feed. Untagged on purpose: 0, 3, 6, 7, 8, 16, 62, 1311, 30009, 10000-series, 30078, and any NIP-04 / NIP-44 encrypted kind. #### Querying The Agora activity feed combines a `t:agora`-strict layer with an intentionally cross-client world layer: ```json [ { "kinds": [33863, 36639, 34550, 8333], "#t": ["agora", "Agora"] }, { "kinds": [1111], "#t": ["agora", "Agora"], "#K": ["33863", "36639", "34550"] }, { "kinds": [1111, 1068], "#k": ["iso3166", "geo"] }, { "kinds": [1], "#t": ["agora", "Agora"] } ] ``` The first two filters surface only Agora-created content. The third surfaces all country/geo-rooted comments and polls regardless of origin — the world layer is intentionally cross-client. The fourth captures any kind 1 note carrying `#agora` (including hashtags users type themselves), which preserves viral / opt-in discovery. Clients filter both case variants (`agora` and `Agora`) because Nostr `t` tags are conventionally lowercase but some clients normalize hashtags to title case. #### Backward compatibility Events published before this marker was adopted do not carry `t:agora` and therefore do not appear in the Agora activity feed. They remain reachable by direct link and via kind-specific directories (e.g. the moderator-curated `/campaigns/all`). Authors who wish to surface a legacy event in the feed can republish it (any edit through the Agora UI will add the marker automatically). ### Community Chat Agora uses NIP-53 live chat messages (`kind:1311`) for realtime chat inside a NIP-72 community. Messages are scoped directly to the community definition's address using an `a` tag: ```json { "kind": 1311, "content": "Hello community!", "tags": [ ["a", "34550::", "", "root"] ] } ``` Clients SHOULD query community chat with `{ "kinds": [1311], "#a": ["34550::"] }`. Agora treats sending as members-only at the UI layer and applies the same community moderation overlay used for community posts. ### Community Kinds These event kinds were created by community contributors and are supported by Ditto. Full specifications are maintained by their respective authors. | Kind | Name | Description | Spec | |-------|------------------------|------------------------------------------------------------------|-------------------------------------------------------------------------------------------| | 2473 | Bird Detection | Bird-by-ear observation log (species heard in the wild) | [NIP](https://gitlab.com/alexgleason/birdstar/-/blob/main/NIP.md) | | 12473 | Birdex | Author's cumulative life list of confirmed bird species | [NIP](https://gitlab.com/alexgleason/birdstar/-/blob/main/NIP.md) | | 3367 | Color Moment | Color palette post expressing a mood | [NIP](https://gitlab.com/chad.curtis/espy/-/blob/main/NIP.md) | | 4223 | Weather Reading | Sensor readings from a weather station | [Draft NIP](https://github.com/nostr-protocol/nips/pull/2163) | | 7516 | Found Log | Log entry recording a user finding a geocache | [NIP-GC](https://gitlab.com/chad.curtis/treasures/-/blob/main/NIP-GC.md) | | 8211 | Encrypted Letter | Encrypted personal letter with visual stationery | [NIP](https://gitlab.com/chad.curtis/lief/-/blob/main/NIP.md) | | 16158 | Weather Station | Weather station metadata (location, sensors, connectivity) | [Draft NIP](https://github.com/nostr-protocol/nips/pull/2163) | | 37516 | Geocache | Geocache listing for real-world treasure hunting | [NIP-GC](https://gitlab.com/chad.curtis/treasures/-/blob/main/NIP-GC.md) | | 36787 | Music Track | Addressable event for a music audio file with metadata | See [Music Tracks & Playlists](#music-tracks--playlists) below | | 34139 | Music Playlist | Ordered list of music track references (also used for albums) | See [Music Tracks & Playlists](#music-tracks--playlists) below | | 30621 | Custom Constellation | User-drawn star figure with Hipparcos-numbered edges | [NIP](https://gitlab.com/alexgleason/birdstar/-/blob/main/NIP.md) | --- ## Kind 8333: Onchain Zap ### Summary Regular event kind that records a **Bitcoin on-chain payment** ("onchain zap") sent in appreciation of a Nostr event or profile. Functions as the on-chain analogue of NIP-57 zap receipts (kind 9735), but without the LNURL round-trip: the event is self-attested by the sender and references a real Bitcoin transaction that clients can verify directly on-chain. The kind number mirrors the convention of NIP-57: kind **9735** is the Lightning P2P port (per BOLT spec), and kind **8333** is the Bitcoin mainnet P2P port — a natural semantic pairing for Lightning vs. on-chain settlement. Because every Nostr keypair deterministically maps to a Bitcoin Taproot (P2TR) address (both use 32-byte x-only secp256k1 keys, per BIP-340/BIP-341), an on-chain zap is simply a Bitcoin transaction whose output pays the recipient's derived Taproot address. The kind 8333 event links that transaction to the Nostr event or profile being zapped. ### Event Structure Single-recipient zap (the common case — tipping a post or profile): ```json { "kind": 8333, "pubkey": "", "content": "Great post!", "tags": [ ["e", "", ""], ["p", ""], ["i", "bitcoin:tx:"], ["amount", ""], ["alt", "On-chain zap: 25000 sats"] ] } ``` Multi-recipient zap (one transaction paying multiple recipients — community splits): ```json { "kind": 8333, "pubkey": "", "content": "Great community!", "tags": [ ["i", "bitcoin:tx:"], ["p", ""], ["p", ""], ["p", ""], ["amount", ""], ["a", "34550::"], ["K", "34550"], ["alt", "Donation: 75000 sats across 3 recipients"] ] } ``` Campaign donation (one transaction paying a single campaign wallet — see Kind 33863 below): ```json { "kind": 8333, "pubkey": "", "content": "Keep up the good work.", "tags": [ ["i", "bitcoin:tx:"], ["amount", ""], ["a", "33863::"], ["K", "33863"], ["alt", "Donation to Save the Last Bookstore: 25000 sats"] ] } ``` Campaign donation receipts MUST NOT include `p` tags — campaigns no longer have Nostr-identity recipients, only a `w` wallet endpoint. Verification matches tx outputs against the campaign's declared `w` address rather than derived Taproot addresses (see *Verification* and Kind 33863 below). ### Content The `content` field is a human-readable comment from the sender (may be empty). It is NOT a zap request JSON (unlike NIP-57 kind 9735). ### Tags | Tag | Required | Description | |----------|----------|----------------------------------------------------------------------------------------------| | `i` | Yes | NIP-73 external content identifier. MUST be `bitcoin:tx:` where `` is a 64-char lowercase hex Bitcoin transaction ID. | | `p` | Yes (≥1) | 32-byte hex pubkey of a zap **recipient** (an author being paid). A single event MAY include multiple `p` tags when the transaction has one output per recipient — each `p` tag MUST correspond to at least one tx output paying that recipient's derived Taproot address. | | `amount` | Yes | **Total** amount paid in **satoshis** (decimal integer). This is the sum of outputs in the tx that paid the derived Taproot addresses of **all** listed `p` recipients combined — *not* the total tx value (it excludes the sender's change output). For single-recipient events this is the amount paid to that one recipient. | | `e` | If zapping an event | 32-byte hex ID of the event being zapped. Include a relay hint as the 3rd element where possible. | | `a` | If zapping an addressable event | Addressable event coordinate `::`. Used instead of (or alongside) `e` for kinds 30000–39999. | | `alt` | Yes | NIP-31 human-readable fallback. | If neither `e` nor `a` is present, the zap targets the recipients' **profiles** (i.e. a tip to the pubkey(s), not to a specific event). ### Publishing Flow 1. Sender builds a Bitcoin transaction with one output per intended recipient, each paying the recipient's derived Taproot address (`nostrPubkeyToBitcoinAddress(recipientPubkey)`). 2. Sender broadcasts the transaction to the Bitcoin network and obtains the `txid`. 3. Sender signs and publishes a **single** kind 8333 event referencing that `txid` with one `p` tag per recipient and an `amount` tag carrying the total paid across all of them. 4. The event is published **after** broadcast; the txid is already final at that point. ### Batch / Community Zaps A single Bitcoin transaction MAY pay multiple recipients by including one output per recipient. Clients SHOULD publish **one kind 8333 event per transaction**, listing every recipient under its own `p` tag and putting the combined total in the single `amount` tag. Per-recipient amounts are not encoded in the event — clients that need them recompute them from the on-chain transaction during verification (each `p` tag's derived Taproot address is matched against tx outputs). For community-level zaps, clients MAY include the community addressable coordinate in an `a` tag and the community kind in a `K` tag: ```json [ ["i", "bitcoin:tx:"], ["p", ""], ["p", ""], ["amount", "5000"], ["a", "34550::"], ["K", "34550"], ["alt", "Bitcoin zap: 5000 sats across 2 recipients"] ] ``` The `amount` tag MUST be the sum of outputs paying the listed recipients; it MUST NOT include the sender's change output. ### Client Behavior **Querying onchain zaps for an event:** ```json { "kinds": [8333], "#e": [""], "limit": 100 } ``` For addressable events, use `"#a": ["::"]` instead. For profile-level zaps targeting a specific user, use `"#p": [""]` — this matches both single-recipient events tagging that user and multi-recipient events where the user is one of several recipients. **Verification (REQUIRED before trusting amounts):** Clients MUST verify a kind 8333 event on-chain before counting it toward a zap total or displaying its amount. The `amount` tag is self-reported by the sender and would otherwise be trivially spoofable. Verification has two modes depending on the event shape: *Identity-recipient mode* (the event has `p` tags — profile zaps, event zaps, community splits): 1. Extract the txid from the `i` tag. 2. Fetch the transaction from a Bitcoin data source (e.g. a mempool.space-compatible Esplora API). 3. For each `p` tag, derive the recipient's expected Taproot address. 4. Sum the values of all outputs in the transaction that pay any of the derived recipient addresses. This is the **verified amount**. Change outputs paying back to the **sender's** derived Taproot address MUST NOT be counted toward the verified amount — only outputs to listed recipients. *Campaign-wallet mode* (the event has an `a` tag pointing at a kind 33863 campaign and no `p` tags): 1. Extract the txid from the `i` tag and the campaign coordinate from the `a` tag. 2. Fetch the campaign event and read its `w` tag to get the campaign's declared bech32(m) wallet address. Reject the receipt if `w` is missing, malformed, or starts with `sp1` (silent-payment campaigns do not publish receipts; see Kind 33863). 3. Fetch the transaction from a Bitcoin data source. 4. Sum the values of all outputs in the transaction that pay the campaign's `w` address. This is the **verified amount**. In both modes: 5. If the verified amount is 0, the event SHOULD be discarded. 6. If the sender's `amount` tag exceeds the verified amount, clients MAY discard the event or MAY display the smaller verified amount (capping). Clients MUST NOT display or count the claimed amount when it exceeds the verified amount. 7. Unconfirmed transactions MAY be displayed as pending; clients MAY require confirmation before counting them toward public totals. Because unconfirmed transactions can be evicted (RBF, double-spend), clients SHOULD either exclude them from aggregate zap totals or clearly label them as pending. When a client needs to attribute a multi-recipient event's amount to one specific recipient (e.g. rendering a profile zap-history entry), it MAY sum only the tx outputs paying that one recipient's derived Taproot address. Per-recipient amounts are not stored in the event — they are recomputed from the transaction at display time. **Sender/recipient identity:** Clients SHOULD reject events where the sender's pubkey (`event.pubkey`) appears in any `p` tag. Self-zaps are trivial to fabricate (the sender already controls the destination address) and contribute nothing meaningful to zap totals. Outputs in the underlying transaction that pay the sender's own derived Taproot address are change outputs and MUST NOT be counted toward the verified amount regardless of the tag set. **Deduplication:** Clients SHOULD deduplicate events that reference the same `txid` (an attacker could publish many events pointing at one real transaction). One kind 8333 event per `txid` is canonical — when multiple events reference the same `txid`, the earliest is preferred. **Network scope:** This specification applies to Bitcoin **mainnet** only. Testnet, signet, and other networks are out of scope; addresses and txids on those networks MUST NOT be used in kind 8333 events. ### Comparison with NIP-57 (Lightning Zaps) | Aspect | NIP-57 (kind 9735) | This spec (kind 8333) | |--------|---------------------|------------------------| | Settlement | Lightning Network | Bitcoin L1 | | Invoice / payment | LNURL + BOLT-11 invoice | Raw Bitcoin tx | | Event issuer | Recipient's LNURL provider | Sender | | Availability | Requires `lud06`/`lud16` on recipient profile | Always available (every Nostr pubkey has a derived Taproot addr) | | Verification | Recipient zap-provider pubkey + bolt11 amount | On-chain tx verified against derived recipient address | | Finality | Instant | Confirms in ~10 min (mempool first) | | Fees | Sub-satoshi typical | Significant at low amounts | The two zap kinds are complementary. Clients SHOULD sum verified amounts from both kinds when displaying total zap stats for a post or profile. --- ## Kind 33863: Campaign ### Summary Addressable event representing a **self-authored fundraising campaign**. A campaign carries marketing-style metadata (title, summary, banner image, markdown story, optional goal, optional deadline, optional country) and exactly one Bitcoin wallet endpoint declared in a `w` tag. The wallet endpoint is either a public on-chain bech32(m) address (`bc1q…`, `bc1p…`) or a silent-payment code (`sp1…`, per BIP-352). The mode is inferred from the prefix — the client renders the corresponding QR code and adjusts the donation-progress UI accordingly. The author of the event is also the beneficiary. Campaigns are never authored on behalf of someone else; the event creator owns the wallet declared in `w` and receives the donations. To stop accepting donations, the creator publishes a NIP-09 kind 5 deletion request referencing the campaign's `a` coordinate. The kind is addressable so the creator can edit the story, banner, goal, deadline, and wallet over the life of the campaign without minting new identifiers. The `d` tag is the campaign's slug. ### Event Structure ```json { "kind": 33863, "pubkey": "", "content": "", "tags": [ ["d", "save-the-last-bookstore"], ["title", "Save the Last Bookstore"], ["summary", "Help our 40-year-old neighborhood bookstore make rent through winter."], ["banner", "https://blossom.example/abc123.jpg"], ["imeta", "url https://blossom.example/abc123.jpg", "m image/jpeg", "x abc123def456...", "dim 1600x900", "blurhash LKO2?U%2Tw=w]~RBVZRi};RPxuwH", "alt Storefront of the Last Bookstore at dusk" ], ["alt", "Fundraising campaign: Save the Last Bookstore"], ["w", "bc1p7w2k3xq9...xyz"], ["goal", "25000"], ["deadline", "1735689600"], ["i", "iso3166-1:US"], ["k", "iso3166-1"] ] } ``` A silent-payment campaign is identical except the `w` tag carries an `sp1…` code: ```json ["w", "sp1qq...verylongsilentpaymentcode..."] ``` ### Content The `content` field is the **campaign story**, formatted as Markdown. Clients SHOULD render it with the same Markdown renderer they use for NIP-23 long-form content. Empty content is permitted (e.g. for a campaign that lives entirely in its summary). ### Tags | Tag | Required | Description | |-----------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `d` | Yes | Campaign slug, unique per author. Forms the addressable coordinate `33863::`. | | `title` | Yes | Display title of the campaign (plain text, max ~200 chars). | | `w` | Yes | Bitcoin wallet endpoint. The 2nd element is a single bech32(m) string: a mainnet on-chain address starting with `bc1q` (P2WPKH/P2WSH) or `bc1p` (P2TR), **or** a silent-payment code starting with `sp1` per BIP-352. Exactly one `w` tag per campaign. | | `summary` | Recommended | Short one-paragraph tagline shown in feed cards and previews. | | `banner` | Recommended | HTTPS URL of the wide banner image. Clients MUST sanitize the URL (see `sanitizeUrl()` in `nostr-security`) before rendering, and SHOULD pair the URL with a NIP-92 `imeta` tag for dimensions, blurhash, MIME type, and SHA-256. | | `imeta` | Recommended | NIP-92 media metadata for the banner. The first `url ` pair MUST match the `banner` URL; clients SHOULD ignore an `imeta` whose URL does not match. | | `goal` | Optional | Fundraising goal in **integer US Dollars** (no unit suffix, no decimals). Clients MAY display an estimated sat-equivalent at view time using a live exchange rate. | | `deadline`| Optional | Unix timestamp (seconds) at which the campaign closes for new donations. After the deadline, clients SHOULD show the campaign as ended but MAY still accept donations. | | `i` | Recommended | NIP-73 country identifier. SHOULD be `iso3166-1:` with an uppercase ISO 3166-1 alpha-2 country code (e.g. `iso3166-1:VE`). | | `k` | Recommended if `i` is present | NIP-73 external content kind. For country identifiers this SHOULD be `iso3166-1`. | | `alt` | Recommended | NIP-31 human-readable fallback. | ### Wallet Modes The prefix of the `w` value selects one of two donation modes. Clients MUST detect the mode from the prefix; the event carries no other mode discriminator. | Prefix | Mode | Description | |---------------------|-----------|------------------------------------------------------------------------------------------------------------------------------------------| | `bc1q…` / `bc1p…` | On-chain | Public mainnet bech32(m) address. Donations are traceable; clients show a progress bar, total raised, and donation list. | | `sp1…` | Silent payment | BIP-352 silent-payment code. Donations are **unlinkable by design**. Clients MUST hide all aggregate totals and progress UI (see below). | Other prefixes (`tb1…`, `bcrt1…`, `tsp1…`, lightning invoices, etc.) MUST be rejected at parse time; the campaign does not render. Clients SHOULD validate the bech32(m) checksum of the `w` value, not just its prefix. ### Client Behavior by Mode | UI element | On-chain (`bc1`) | Silent payment (`sp1`) | |-----------------------------|-----------------------------------------------------------------|-------------------------------------------------------| | QR code | bech32(m) address QR (or BIP-21 `bitcoin:` URI) | SP code QR (BIP-352 / BIP-21 SP extension) | | "Raised X" / progress bar | Shown, computed from verified kind 8333 receipts | **Hidden.** Replaced with a "Private campaign — totals are not public" notice. | | Donor / recent-donation list| Shown | **Hidden.** | | Goal display | Shown as USD target with optional sat-equivalent estimate | Shown as USD target; no progress computation | | Donation receipt published | Donor's client publishes a kind 8333 receipt (see below) | **No receipt published.** Publishing one would defeat SP unlinkability and is forbidden. | For silent-payment campaigns, clients MUST NOT attempt to scan the chain, MUST NOT publish receipts, and MUST NOT display any aggregate that could leak donation activity. The only signal the public sees is the campaign event itself. ### Donation Flow — On-chain (`bc1`) 1. Donor opens the campaign and chooses an amount. 2. Donor's client constructs and broadcasts a Bitcoin transaction paying the campaign's `w` address. 3. After broadcast, the donor's client publishes a single kind 8333 receipt: ```json [ ["i", "bitcoin:tx:"], ["amount", ""], ["a", "33863::"], ["K", "33863"], ["alt", "Donation to : sats"] ] ``` The receipt MUST NOT carry `p` tags — campaigns are not Nostr-identity recipients. The `amount` tag is the sum of tx outputs paying the campaign's `w` address (excluding the donor's change output). 4. The receipt is published **after** the tx is broadcast; the txid is already final at that point. A receipt-publish failure does not roll back the donation — the on-chain transaction stands. ### Donation Flow — Silent Payment (`sp1`) 1. Donor opens the campaign and chooses an amount. 2. Donor's client uses the campaign's SP code to derive a fresh, one-time Taproot output script per BIP-352. 3. Donor broadcasts a Bitcoin transaction paying that derived output. 4. **No Nostr event is published.** The campaign owner discovers the donation by scanning the chain locally with their SP private key. Silent-payment unlinkability is the entire point of this mode. Clients MUST NOT publish receipts, MUST NOT advertise the donation in any other Nostr event (replies, mentions, etc.) on the donor's behalf, and MUST NOT correlate the donor's pubkey with the campaign in any persisted client telemetry. ### Querying **List campaigns (newest first):** ```json { "kinds": [33863], "limit": 50 } ``` **Fetch a specific campaign:** ```json { "kinds": [33863], "authors": [""], "#d": [""], "limit": 1 } ``` **Aggregate donations for an on-chain campaign:** ```json { "kinds": [8333], "#a": ["33863::"], "limit": 500 } ``` Clients MUST verify each kind 8333 event on-chain before counting it toward the campaign total, per the *Campaign-wallet mode* verification rules in the kind 8333 section. **Filter by country:** ```json { "kinds": [33863], "#i": ["iso3166-1:VE"], "limit": 50 } ``` **Fetch pinned event comments:** Event owners MAY pin important comments or activity feed events with a NIP-78 app-specific data event (`kind: 30078`) authored by the root event owner. The `d` tag is scoped to the root event coordinate. Agora uses this for campaigns (`33863`), pledges (`36639`), organizations (`34550`), and calendar events (`31922` / `31923`). ```json { "kind": 30078, "pubkey": "", "content": "{\"pinnedEvents\":[\"\",\"\"]}", "tags": [ ["d", "agora-pinned-comments:::"], ["a", "::"], ["k", ""], ["alt", "Pinned event comments"] ] } ``` Clients SHOULD query the pin list with: ```json { "kinds": [30078], "authors": [""], "#d": ["agora-pinned-comments:::"], "limit": 1 } ``` The `pinnedEvents` array is ordered newest pin first. Pinning an already-pinned event removes it. Clients SHOULD ignore pin lists not authored by the root event owner. ### Client Behavior - **Wallet validity:** clients MUST reject events whose `w` tag is missing, present more than once, or whose value does not pass bech32(m) checksum validation for one of the supported prefixes. Invalid campaigns do not render. - **Editability:** the creator MAY republish the same `(33863, pubkey, d)` triple to update any field, including the `w` wallet endpoint. Clients SHOULD keep `published_at` from the first publish on subsequent edits (NIP-23 convention). - **Closing a campaign:** there is no `status` tag. To stop accepting donations, the creator publishes a NIP-09 kind 5 deletion request referencing the campaign's `a` coordinate. Clients SHOULD honor the deletion by removing the campaign from discovery feeds. Historical kind 8333 receipts MAY still be rendered against the (now-deleted) campaign coordinate so donors can find their past donations. - **No category, no topics:** kind 33863 events MUST NOT carry `t` tags or NIP-32 category labels in any `agora.*` namespace. Campaigns are individual stories; discovery happens via search (NIP-50 against title/summary/content), country (`#i`), and moderator curation (below). - **Migration:** kind 33863 has no relationship to any earlier campaign kind. Clients MUST NOT read, merge, or migrate events of any other kind into the kind 33863 namespace. ### Agora Moderation Labels Agora curates which kind 33863 campaigns appear on the homepage (`/`) and on the Support directory (`/campaigns/all`), and which kind 34550 organizations appear in the Featured shelf on `/communities`, via moderator-signed NIP-32 label events (kind 1985) in a dedicated label namespace. The labeled event itself is never modified — surfacing is purely a client-side rollup of label events. Campaigns and organizations share a single label namespace and a single moderator pack (Team Soapbox); the only thing distinguishing the two streams is the kind prefix on the `a` tag of each label: - `33863::` — campaign (kind 33863, see "Open Campaigns" above). - `34550::` — organization (kind 34550, NIP-72 community definition). A client surfacing campaigns MUST filter folded labels to those whose `a` tag starts with `33863:`. A client surfacing organizations MUST filter to `34550:`. Mixing the two streams would let a moderator's `featured` label on a campaign appear to feature an unrelated organization with the same `d` tag, or vice versa. #### Namespace ``` agora.moderation ``` Each label event carries the namespace twice, per NIP-32: - A capital-`L` "namespace" tag (relay-indexed for queries). - A lowercase `l` tag where the 2nd element is the label value and the 3rd is the namespace. #### Label values Three independent axes are defined; the newest moderator-signed label per axis per coordinate wins. **Campaigns** use all three axes (`approval`, `hide`, `featured`). **Organizations** use only two — `hide` and `featured` — because every Agora-tagged organization is publicly visible by default; there is no approval gate for orgs. Moderators MUST NOT publish `approved` or `unapproved` labels against kind 34550 coordinates, and clients MUST ignore any such labels they receive. | Axis | Values | Surfaces | Meaning | |----------|---------------------------|----------------|-------------------------------------------------------------------------| | approval | `approved`, `unapproved` | campaigns only | `approved` allows the campaign on its discovery surfaces. `unapproved` retracts a previous approval. | | hide | `hidden`, `unhidden` | both | `hidden` suppresses the campaign/organization everywhere it would otherwise appear. `unhidden` retracts a previous hide. | | featured | `featured`, `unfeatured` | both | `featured` places the campaign in the hand-picked Featured row on `/`, or the organization in the Featured shelf on `/communities`. `unfeatured` retracts. | Surfacing rules (hide always wins): **Campaigns** - **Featured row on `/`** — iff the latest featured label is `featured` AND the latest hide label is not `hidden`. Ordered newest-`created_at`-of-`featured`-label first. Featured is independent of Approved at the protocol level; a campaign may be featured without being approved (the home page treats Featured and Approved as deduplicated bins, with Featured taking precedence). - **Community Campaigns grid on `/`** — iff approved, not hidden, and not featured (featured campaigns get their own row above). - **Discover shelf** — iff approved AND not hidden. - **Moderator-only "Pending"** — iff neither approved nor hidden. - **Moderator-only "Hidden"** — iff hidden. **Organizations** - **Featured shelf on `/communities`** — iff the latest featured label is `featured` AND the latest hide label is not `hidden`. Ordered newest-`created_at`-of-`featured`-label first. - **"My organizations" shelf on `/communities`** — intentionally ignores all moderation labels. A user's own founded, moderated, or followed organizations always render regardless of label state. - **Moderator-only "Needs review"** — iff `t:agora` AND not featured AND not hidden. Surfaces orgs minted through Agora's create flow that haven't been triaged into Featured or Hidden yet. - **Moderator-only "Hidden"** — iff hidden. - **Hide enforcement on other organization discovery surfaces** — clients SHOULD suppress `hidden` organizations from any future "All organizations" / browse surface for non-moderators. Moderators MAY see hidden organizations with a "Hidden" treatment so they can unhide. #### Event Structure ```json { "kind": 1985, "content": "", "tags": [ ["L", "agora.moderation"], ["l", "approved", "agora.moderation"], ["a", "33863::"], ["alt", "Campaign moderation: approved"] ] } ``` An organization label has the same shape with a kind 34550 `a` tag: ```json { "kind": 1985, "content": "", "tags": [ ["L", "agora.moderation"], ["l", "featured", "agora.moderation"], ["a", "34550::"], ["alt", "Organization moderation: featured"] ] } ``` Required tags: - `L` set to `agora.moderation`. - `l` with the label value as the 2nd element and `agora.moderation` as the 3rd. - `a` referencing the target coordinate (`33863::` for a campaign, `34550::` for an organization). - `alt` (NIP-31) — clients without label support will display this string. The `alt` value SHOULD identify the surface (e.g. `Campaign moderation: featured` or `Organization moderation: featured`) so non-Agora clients can read it. #### Trust Model Only label events authored by current members of the **Team Soapbox** follow pack are honored. The pack is a kind 39089 (NIP-51 follow pack) addressable event: ``` kind: 39089 pubkey: 932614571afcbad4d17a191ee281e39eebbb41b93fac8fd87829622aeb112f4d d-tag: k4p5w0n22suf ``` The pack `p` tags are the authoritative moderator list. Clients MUST pin `authors:` on their label REQ to the pack `p` tags; events from non-pack authors MUST be ignored. This means: - Self-approval is impossible unless the pack author has added you. - A moderator removed from the pack immediately loses moderation authority — campaigns/organizations kept alive only by their labels return to "pending" until another moderator approves them. - The pack author (single signer) can reset the entire moderator roster by republishing the pack. The same moderator set governs both campaign and organization labels. Carving out per-surface moderator subsets is out of scope; clients that need that distinction would have to introduce a second follow pack and a second label namespace. #### Querying Step 1 — fetch the pack: ```json { "kinds": [39089], "authors": ["932614571afcbad4d17a191ee281e39eebbb41b93fac8fd87829622aeb112f4d"], "#d": ["k4p5w0n22suf"], "limit": 1 } ``` Step 2 — fetch label events from pack members in the namespace: ```json { "kinds": [1985], "authors": ["", "", "..."], "#L": ["agora.moderation"], "limit": 2000 } ``` Step 3 — fold by `(coord, axis)`, latest-`created_at`-wins, filtering to the relevant kind prefix (`33863:` for campaigns or `34550:` for organizations). Then fetch the targeted events themselves — one filter per author (bundled in a single REQ) keyed by their d-tags. #### Client Behavior - Clients SHOULD render approve/hide/feature controls only for users whose pubkey appears in the pack. - Clients MAY display "Hidden" badges on hidden campaigns/organizations when viewed by a moderator, and SHOULD NOT render them at all to non-moderators. - Non-moderator authors viewing the homepage SHOULD see their own pending campaigns in a separate explained section so they understand why their campaign isn't yet on the homepage. The campaign URL remains live and donatable regardless of moderation state. - Organization authors are not shown an equivalent "pending" surface today — organizations are visible at their NIP-19 route regardless of moderation, and the only moderation surface is the Featured shelf. --- ## Kind 16769: Profile Tabs ### Summary Replaceable event kind for publishing a user's custom profile page tabs. Exactly one event per user (no `d` tag). Each tab defines a Nostr filter (NIP-01) that clients execute to populate the tab's content. Visitors who load a profile fetch this event to display the custom tabs alongside the standard Posts / Media / Likes / Wall tabs. ### Event Structure ```json { "kind": 16769, "content": "", "tags": [ ["var", "$follows", "p", "a:3:$me:"], ["tab", "Bitcoin Posts", "{\"kinds\":[1],\"authors\":[\"$me\"],\"search\":\"bitcoin\"}"], ["tab", "Feed", "{\"kinds\":[1,6],\"authors\":[\"$follows\"],\"limit\":40}"], ["alt", "Custom profile tabs"] ] } ``` ### Content The `content` field is unused and MUST be an empty string (`""`). ### Tags | Tag | Format | Description | |-------|------------------------------------------------|----------------------------------------------------------------| | `tab` | `["tab", "