--- name: welshman-util description: "Use this skill when working with @welshman/util: nostr event types, kinds, tags, filters, addresses, NIPs (42/86/98), profiles, relays, zaps, wallets, or any core nostr data structures." --- # welshman/util — Core Nostr Utilities `@welshman/util` is the foundational layer of the welshman nostr stack, providing types, constants, and helpers for every nostr primitive: events, kinds, tags, filters, addresses, profiles, lists, zaps, relays, and Lightning wallet integration. Higher level welshman packages (`@welshman/net`, `@welshman/app`, `@welshman/store`, etc.) depend on the types and utilities defined here. ## Installation ```bash npm install @welshman/util # or pnpm add @welshman/util # or yarn add @welshman/util ``` --- ## Key Exports ### Event Types | Type | Description | |------|-------------| | `EventContent` | `{ tags, content }` — base content structure | | `EventTemplate` | `EventContent + kind` | | `StampedEvent` | `EventTemplate + created_at` | | `OwnedEvent` | `StampedEvent + pubkey` | | `HashedEvent` | `OwnedEvent + id` | | `SignedEvent` | `HashedEvent + sig` | | `TrustedEvent` | `HashedEvent + optional sig` — most common in-app type | | `DecryptedEvent` | `TrustedEvent + plaintext` (for encrypted lists/events) | ### Event Utilities | Export | Description | |--------|-------------| | `verifiedSymbol` | Symbol (re-exported from `nostr-tools`) used as a key on events; set `event[verifiedSymbol] = true` to skip signature re-validation | | `makeEvent(kind, opts?)` | Create a `StampedEvent` with optional content, tags, created_at | | `verifyEvent(event)` | Verify event signature; returns `false` for unsigned events (no `sig` field) even if `verifiedSymbol` is set, because `isSignedEvent` is checked first; returns `true` immediately for signed events where `event[verifiedSymbol]` is already set | | `getIdentifier(event)` | Get `d` tag value | | `getIdOrAddress(event)` | Returns address string for replaceable events, id otherwise | | `getIdAndAddress(event)` | Returns array with both id and address (if applicable) | | `deduplicateEvents(events)` | Deduplicate by id or address | | `isEphemeral(event)` | True for ephemeral kinds (20000–29999) | | `isReplaceable(event)` | True for plain or parameterized replaceable | | `isPlainReplaceable(event)` | True for kinds 10000–19999 and metadata/contacts | | `isParameterizedReplaceable(event)` | True for kinds 30000–39999 | | `getAncestors(event)` | Returns `{ roots, replies, mentions }` for NIP-10 events (mentions may be empty `[]` but is always present); NIP-22/COMMENT path returns `{ roots, replies }` without mentions | | `getParentIdOrAddr(event)` | Immediate parent id or address | | `isChildOf(child, parent)` | Check if child replies to parent | ### Type Guards `isEventTemplate`, `isStampedEvent`, `isOwnedEvent`, `isHashedEvent`, `isSignedEvent` ### Event Kinds (constants) All constants are exported by name from `@welshman/util`. **Core / NIP-01** ``` PROFILE = 0 NOTE = 1 FOLLOWS = 3 DELETE = 5 REPOST = 6 REACTION = 7 BADGE_AWARD = 8 MESSAGE = 9 THREAD = 11 SEAL = 13 DIRECT_MESSAGE = 14 DIRECT_MESSAGE_FILE = 15 GENERIC_REPOST = 16 PICTURE_NOTE = 20 VANISH = 62 COMMENT = 1111 GENERIC_REPOST = 16 ``` **Channels (NIP-28)** ``` CHANNEL_CREATE = 40 CHANNEL_UPDATE = 41 CHANNEL_MESSAGE = 42 CHANNEL_HIDE_MESSAGE = 43 CHANNEL_MUTE_USER = 44 ``` **Wrapped / encrypted (NIP-59)** ``` WRAP = 1059 WRAP_NIP04 = 1060 WRAPPED_KINDS = [DIRECT_MESSAGE, DIRECT_MESSAGE_FILE] // convenience array ``` **Media / files** ``` FILE_METADATA = 1063 PICTURE_NOTE = 20 AUDIO = 31337 ``` **Polls** ``` POLL = 1068 POLL_RESPONSE = 1018 ``` **Marketplace / auction** ``` BID = 1021 BID_CONFIRMATION = 1022 STALL = 30017 PRODUCT = 30018 MARKET_UI = 30019 PRODUCT_SOLD_AS_AUCTION = 30020 CLASSIFIED = 30402 DRAFT_CLASSIFIED = 30403 ``` **Git (NIP-34)** ``` GIT_PATCH = 1617 GIT_ISSUE = 1621 GIT_REPLY = 1622 GIT_STATUS_OPEN = 1630 GIT_STATUS_COMPLETE = 1631 GIT_STATUS_CLOSED = 1632 GIT_STATUS_DRAFT = 1633 GIT_REPOSITORY = 30403 ``` **Social / community** ``` REMIX = 1808 REPORT = 1984 LABEL = 1985 REVIEW = 1986 HIGHLIGHT = 9802 APPROVAL = 4550 NOSTROCKET_PROBLEM = 1971 COMMUNITY = 34550 BADGE_DEFINITION = 30009 BADGES = 30008 LIVE_EVENT = 30311 LIVE_CHAT_MESSAGE = 1311 ``` **Rooms (NIP-29)** ``` ROOM_CREATE = 9007 ROOM_DELETE = 9008 ROOM = 35834 ROOM_JOIN = 9021 ROOM_LEAVE = 9022 ROOM_META = 39000 ROOM_ADMINS = 39001 ROOM_MEMBERS = 39002 ROOM_EDIT_META = 9002 ROOM_ADD_MEMBER = 9000 ROOM_REMOVE_MEMBER = 9001 ROOM_ADD_PERM = 9003 ROOM_REMOVE_PERM = 9004 ROOM_DELETE_EVENT = 9005 ROOM_EDIT_STATUS = 9006 ROOM_CREATE_PERMISSION = 19004 RELAY_MEMBERS = 13534 RELAY_ADD_MEMBER = 8000 RELAY_REMOVE_MEMBER = 8001 RELAY_JOIN = 28934 RELAY_INVITE = 28935 RELAY_LEAVE = 28936 ``` **Replaceable lists (kinds 10000–10099)** ``` MUTES = 10000 PINS = 10001 RELAYS = 10002 BOOKMARKS = 10003 COMMUNITIES = 10004 CHANNELS = 10005 BLOCKED_RELAYS = 10006 SEARCH_RELAYS = 10007 ROOMS = 10009 FEEDS = 10014 TOPICS = 10015 EMOJIS = 10030 MESSAGING_RELAYS = 10050 BLOSSOM_SERVERS = 10063 FILE_SERVERS = 10096 ``` **Parameterized replaceable lists (kinds 30000–30102)** ``` NAMED_PEOPLE = 30000 NAMED_RELAYS = 30002 NAMED_BOOKMARKS = 30003 NAMED_CURATIONS = 30004 NAMED_TOPICS = 30015 NAMED_WIKI_AUTHORS = 30101 NAMED_WIKI_RELAYS = 30102 NAMED_EMOJIS = 30030 NAMED_ARTIFACTS = 30063 NAMED_COMMUNITIES = 30064 ``` **Long-form / wiki / publishing (NIP-23)** ``` LONG_FORM = 30023 LONG_FORM_DRAFT = 30024 WIKI = 30818 APP_DATA = 30078 FEED = 31890 ``` **Calendar (NIP-52)** ``` CALENDAR = 31924 EVENT_DATE = 31922 EVENT_TIME = 31923 EVENT_RSVP = 31925 ``` **Handlers (NIP-89)** ``` HANDLER_INFORMATION = 31990 HANDLER_RECOMMENDATION = 31989 ``` **Status / alerts** ``` STATUS = 30315 ALERT_EMAIL = 32830 ALERT_STATUS = 32831 ALERT_WEB = 32832 ALERT_ANDROID = 32833 ALERT_IOS = 32834 ``` **Zaps / wallet / Lightning** ``` ZAP_GOAL = 9041 ZAP_REQUEST = 9734 ZAP_RESPONSE = 9735 WALLET_INFO = 13194 WALLET_REQUEST = 23194 WALLET_RESPONSE = 23195 LIGHTNING_PUB_RPC = 21000 OTS = 1040 ``` **Auth** ``` CLIENT_AUTH = 22242 BLOSSOM_AUTH = 24242 HTTP_AUTH = 27235 NOSTR_CONNECT = 24133 ``` **Follow packs** ``` FOLLOW_PACK = 39089 ``` **Promenade protocol** ``` PROMENADE_REGISTER_ACCOUNT = 16430 PROMENADE_SHARD_SHARE = 26428 PROMENADE_SHARD_ACK = 26429 PROMENADE_CONFIG = 26430 PROMENADE_COMMIT = 26431 PROMENADE_REQUEST = 26432 PROMENADE_RESULT = 26433 ``` **Deprecated** ``` DEPRECATED_RELAY_RECOMMENDATION = 2 DEPRECATED_DIRECT_MESSAGE = 4 DEPRECATED_NAMED_GENERIC = 30001 ``` **DVM — Data Vending Machines (NIP-90, kinds 5000–7000)** Requests (`5xxx`) and their paired responses (`6xxx`): ``` DVM_REQUEST_TEXT_EXTRACTION = 5000 DVM_RESPONSE_TEXT_EXTRACTION = 6000 DVM_REQUEST_TEXT_SUMMARY = 5001 DVM_RESPONSE_TEXT_SUMMARY = 6001 DVM_REQUEST_TEXT_TRANSLATION = 5002 DVM_RESPONSE_TEXT_TRANSLATION = 6002 DVM_REQUEST_TEXT_GENERATION = 5050 DVM_RESPONSE_TEXT_GENERATION = 6050 DVM_REQUEST_IMAGE_GENERATION = 5100 DVM_RESPONSE_IMAGE_GENERATION = 6100 DVM_REQUEST_VIDEO_CONVERSION = 5200 DVM_RESPONSE_VIDEO_CONVERSION = 6200 DVM_REQUEST_VIDEO_TRANSLATION = 5201 DVM_RESPONSE_VIDEO_TRANSLATION = 6201 DVM_REQUEST_IMAGE_TO_VIDEO_CONVERSION = 5202 DVM_RESPONSE_IMAGE_TO_VIDEO_CONVERSION = 6202 DVM_REQUEST_TEXT_TO_SPEECH = 5250 DVM_RESPONSE_TEXT_TO_SPEECH = 6250 DVM_REQUEST_DISCOVER_CONTENT = 5300 DVM_RESPONSE_DISCOVER_CONTENT = 6300 DVM_REQUEST_DISCOVER_PEOPLE = 5301 DVM_RESPONSE_DISCOVER_PEOPLE = 6301 DVM_REQUEST_SEARCH_CONTENT = 5302 DVM_RESPONSE_SEARCH_CONTENT = 6302 DVM_REQUEST_SEARCH_PEOPLE = 5303 DVM_RESPONSE_SEARCH_PEOPLE = 6303 DVM_REQUEST_COUNT = 5400 DVM_RESPONSE_COUNT = 6400 DVM_REQUEST_MALWARE_SCAN = 5500 DVM_RESPONSE_MALWARE_SCAN = 6500 DVM_REQUEST_OTS = 5900 DVM_RESPONSE_OTS = 6900 DVM_REQUEST_OP_RETURN = 5901 DVM_RESPONSE_OP_RETURN = 6901 DVM_REQUEST_PUBLISH_SCHEDULE = 5905 DVM_RESPONSE_PUBLISH_SCHEDULE = 6905 DVM_FEEDBACK = 7000 ``` Use `isDVMKind(kind)` to test if a kind falls in the DVM range (5000–7000). **Kind classifiers** ```typescript isRegularKind(kind) // 1000–9999 and select low kinds isPlainReplaceableKind(kind) // 0, 3, and 10000–19999 isEphemeralKind(kind) // 20000–29999 isParameterizedReplaceableKind(kind) // 30000–39999 isReplaceableKind(kind) // plain OR parameterized replaceable isDVMKind(kind) // 5000–7000 ``` ### Tags | Export | Description | |--------|-------------| | `getTags(types, tags)` | Get all tags matching one or more type strings | | `getTag(types, tags)` | Get first matching tag | | `getTagValues(types, tags)` | Get value (index 1) of all matching tags — types first, then the tags array | | `getTagValue(types, tags)` | Get value of first matching tag — types first, then the tags array | | `getEventTags(tags)` | `e` tags | | `getEventTagValues(tags)` | Values of `e` tags | | `getAddressTags(tags)` | `a` tags | | `getAddressTagValues(tags)` | Values of `a` tags | | `getPubkeyTags(tags)` | `p` tags | | `getPubkeyTagValues(tags)` | Values of `p` tags | | `getTopicTags(tags)` / `getTopicTagValues(tags)` | `t` (hashtag) tags | | `getRelayTags(tags)` / `getRelayTagValues(tags)` | `r` and `relay` tags | | `getKindTags(tags)` / `getKindTagValues(tags)` | `k` tags (returns `number[]`) | | `getGroupTags(tags)` / `getGroupTagValues(tags)` | group tags | | `getReplyTags(tags)` | `{ roots, replies, mentions }` — NIP-10 threading | | `getCommentTags(tags)` | `{ roots, replies }` — NIP-22 uppercase/lowercase tags | | `uniqTags(tags)` | Remove duplicate tags | | `tagsFromIMeta(imeta)` | Parse `imeta` tag into array of tag arrays | ### Filters | Export | Description | |--------|-------------| | `matchFilter(filter, event)` | Test if event matches a single filter | | `matchFilters(filters, event)` | Test if event matches any filter | | `getIdFilters(idsOrAddresses)` | Build filters from mixed ids and addresses | | `getReplyFilters(events, filter?)` | Build filters to find replies | | `addRepostFilters(filters)` | Add kind 6/16 repost filters | | `unionFilters(filters)` | Merge overlapping filters | | `intersectFilters(groups)` | Intersect arrays of filter groups | | `trimFilter(filter)` / `trimFilters(filters)` | Limit array fields to 1000 items | | `getFilterId(filter)` | Compact string key for a filter | | `getFilterGenerality(filter)` | 0 = specific, 1 = general | | `guessFilterDelta(filters, max?)` | Estimate appropriate time window in seconds | | `getFilterResultCardinality(filter)` | Expected result count for id-based filters | ### Address | Export | Description | |--------|-------------| | `Address` class | Handles `kind:pubkey:identifier` and NIP-19 naddr format | | `Address.isAddress(s)` | Validate address string format | | `Address.from(s, relays?)` | Parse from `kind:pubkey:identifier` string | | `Address.fromNaddr(naddr)` | Parse from NIP-19 naddr | | `Address.fromEvent(event, relays?)` | Create from addressable event | | `address.toString()` | Serialize to `kind:pubkey:identifier` | | `address.toNaddr()` | Serialize to NIP-19 naddr | | `getAddress(event)` | Convenience: get address string from event | ### Profile | Export | Description | |--------|-------------| | `makeProfile(partial)` | Create a profile object | | `readProfile(event)` | Parse `PublishedProfile` from kind 0 event | | `createProfile(profile)` | Create kind 0 `EventTemplate` | | `editProfile(published)` | Update existing profile event | | `displayProfile(profile?, fallback?)` | Get best display name string | | `displayPubkey(pubkey)` | Shorten pubkey to `npub1abc...xyz` | | `profileHasName(profile?)` | Check if profile has a name field | Profile fields: `name`, `display_name`, `about`, `picture`, `banner`, `website`, `nip05`, `lud06`, `lud16`, `lnurl` ### Lists (kind 10000+) | Export | Description | |--------|-------------| | `makeList(params)` | Create a new list | | `readList(event)` | Parse `PublishedList` from `DecryptedEvent` | | `getListTags(list)` | Combined public + private tags | | `addToListPublicly(list, ...tags)` | Returns `Encryptable` with tag added publicly | | `addToListPrivately(list, ...tags)` | Returns `Encryptable` with tag added privately | | `removeFromList(list, value)` | Returns `Encryptable` with tag removed | | `removeFromListByPredicate(list, pred)` | Returns `Encryptable` with matching tags removed | | `updateList(list, { publicTags?, privateTags? })` | Bulk update tags | ### Encryptable | Export | Description | |--------|-------------| | `Encryptable` | Wraps a partial event with plaintext updates; call `.reconcile(encrypt)` to produce encrypted event | | `asDecryptedEvent(event, plaintext?)` | Attach plaintext data to a `TrustedEvent` | ### Relay | Export | Description | |--------|-------------| | `RelayMode` | Enum: `Read`, `Write`, `Search`, `Blocked`, `Messaging` | | `RelayProfile` | NIP-11 relay info type | | `isRelayUrl(url)` | Validate relay URL | | `isShareableRelayUrl(url)` | True if valid relay URL and not a local address | | `isOnionUrl(url)` | Tor address check | | `isLocalUrl(url)` | Local address check | | `isIPAddress(url)` | IP address check | | `normalizeRelayUrl(url)` | Normalize to standard wss:// format | | `displayRelayUrl(url)` | Strip protocol and trailing slash | | `displayRelayProfile(profile?, fallback?)` | Get display name for relay | ### Zaps (NIP-57) | Export | Description | |--------|-------------| | `getLnUrl(address)` | Convert lightning address or URL to LNURL; returns `undefined` if invalid | | `getInvoiceAmount(bolt11)` | Extract millisatoshi amount from BOLT11 invoice | | `hrpToMillisat(hrpString)` | Convert human-readable BTC amount to millisats (`bigint`) | | `zapFromEvent(response, zapper)` | Validate zap receipt and return `Zap` or `undefined` | | `Zapper` type | `{ lnurl, pubkey?, callback?, minSendable?, maxSendable?, nostrPubkey?, allowsNostr? }` | | `Zap` type | `{ request: TrustedEvent, response: TrustedEvent, invoiceAmount: number }` | ### Wallet | Export | Description | |--------|-------------| | `WalletType` | Enum: `WebLN`, `NWC` | | `Wallet` | Union: `WebLNWallet | NWCWallet` | | `isWebLNWallet(wallet)` | Type guard | | `isNWCWallet(wallet)` | Type guard | ### NIP-42 (Relay Auth) ```typescript makeRelayAuth(url: string, challenge: string): StampedEvent // Creates kind 22242 auth event; sign before sending ``` ### NIP-98 (HTTP Auth) ```typescript makeHttpAuth(url: string, method?: string, body?: string): Promise makeHttpAuthHeader(event: SignedEvent): string // Returns "Nostr " ``` ### NIP-86 (Relay Management) ```typescript sendManagementRequest(url: string, request: ManagementRequest, authEvent: SignedEvent): Promise // ManagementResponse = { result?: any; error?: string } // ManagementMethod enum covers: BanPubkey, AllowPubkey, BanEvent, AllowEvent, etc. ``` ### Handlers (NIP-89) ```typescript readHandlers(event: TrustedEvent): Handler[] getHandlerKey(handler: Handler): string // "kind:address" format getHandlerAddress(event: TrustedEvent): string | undefined displayHandler(handler?: Handler, fallback?: string): string ``` ### Links ```typescript fromNostrURI(s: string): string // strips "nostr:" or "nostr://" prefix toNostrURI(s: string): string // ensures "nostr:" prefix ``` ### Blossom (Media Servers) ```typescript makeBlossomAuthEvent(opts: BlossomAuthEventOpts): StampedEvent uploadBlob(server, blob, opts?): Promise getBlob(server, sha256, opts?): Promise deleteBlob(server, sha256, opts?): Promise listBlobs(server, pubkey, opts?): Promise checkBlobExists(server, sha256, opts?): Promise<{exists, size?}> buildBlobUrl(server, sha256, extension?): string encryptFile(file: Blob): Promise decryptFile(encryptedFile: EncryptedFile): Promise ``` --- ## Common Patterns ### Creating and inspecting events ```typescript import { makeEvent, NOTE, PROFILE, RELAYS, LONG_FORM, getIdentifier, getIdOrAddress } from '@welshman/util' // Text note (kind 1) const note = makeEvent(NOTE, { content: 'Hello Nostr!', tags: [['t', 'nostr']], }) // Profile update (kind 0) const profile = makeEvent(PROFILE, { content: JSON.stringify({ name: 'Alice', about: 'Nostr dev' }), tags: [], }) // Relay list (kind 10002) const relayList = makeEvent(RELAYS, { content: '', tags: [ ['r', 'wss://relay.example.com', 'read'], ['r', 'wss://relay2.example.com', 'write'], ], }) ``` ### Pre-verifying persisted events with verifiedSymbol When loading events from a local store (IndexedDB, localStorage, etc.) at startup, you can skip expensive signature re-validation by marking them as already verified: ```typescript import { verifiedSymbol } from '@welshman/util' import type { TrustedEvent } from '@welshman/util' // Load from storage const storedEvents: TrustedEvent[] = await db.getAll('events') // Mark as pre-verified — verifyEvent() will return true immediately (without // re-running the cryptographic check) for events that have a sig field for (const event of storedEvents) { event[verifiedSymbol] = true } repository.load(storedEvents) ``` Only do this for events you persisted yourself after they were validated. Never set `verifiedSymbol` on events received directly from untrusted external sources. ### Working with tags ```typescript import { getTagValue, getTagValues, getPubkeyTagValues, getTopicTagValues, getRelayTagValues, getReplyTags, uniqTags, } from '@welshman/util' // getTagValue and getTagValues: types argument FIRST, then the tags array const title = getTagValue('title', event.tags) // string | undefined const urls = getTagValues('r', event.tags) // string[] // Multiple types at once const ids = getTagValues(['e', 'a'], event.tags) // string[] const mentions = getPubkeyTagValues(event.tags) // string[] const topics = getTopicTagValues(event.tags) // string[] const relays = getRelayTagValues(event.tags) // string[] // NIP-10 thread context const { roots, replies, mentions: threadMentions } = getReplyTags(event.tags) ``` ### Matching and building filters ```typescript import { matchFilters, getIdFilters, getReplyFilters, addRepostFilters, NOTE } from '@welshman/util' import { ago, HOUR } from '@welshman/lib' // Does this event match our subscription? const active = matchFilters([{ kinds: [NOTE], authors: [myPubkey] }], event) // Fetch a set of events by id or address const filters = getIdFilters([ 'abc123', // event id '30023:deadbeef:my-slug', // address ]) // Find all replies to a set of events const replyFilters = getReplyFilters(events, { since: ago(HOUR) }) // Automatically include repost kinds const withReposts = addRepostFilters([{ kinds: [NOTE] }]) ``` ### Addresses for replaceable events ```typescript import { Address, getAddress } from '@welshman/util' // From an addressable event const addr = Address.fromEvent(event, ['wss://relay.example.com']) console.log(addr.toString()) // '30023:deadbeef:my-slug' console.log(addr.toNaddr()) // 'naddr1...' // Round-trip from naddr const parsed = Address.fromNaddr('naddr1...') // Quick string form const addressStr = getAddress(event) // '30023:deadbeef:my-slug' ``` ### Profiles ```typescript import { readProfile, displayProfile, displayPubkey, editProfile } from '@welshman/util' const profile = readProfile(kind0Event) console.log(displayProfile(profile, 'Anonymous')) // name or fallback console.log(displayPubkey(pubkey)) // 'npub1abc...xyz' // Update profile const updatedEvent = editProfile({ ...profile, name: 'New Name', about: 'Updated bio' }) // sign and publish updatedEvent ``` ### Zap flow ```typescript import { getLnUrl, makeEvent, ZAP_REQUEST, zapFromEvent } from '@welshman/util' // Step 1: resolve LNURL const lnurl = getLnUrl('satoshi@getalby.com') if (!lnurl) throw new Error('Invalid lightning address') // Step 2: build zap request (kind 9734) const zapRequest = makeEvent(ZAP_REQUEST, { content: 'Great post!', tags: [ ['p', recipientPubkey], ['e', targetEventId], ['amount', '5000'], // millisats ['lnurl', lnurl], ['relays', 'wss://relay.damus.io'], ], }) // Step 3: sign, send to LNURL callback, pay invoice... // Step 4: validate receipt (kind 9735) const zap = zapFromEvent(zapReceipt, { nostrPubkey: zapperPubkey, allowsNostr: true, lnurl }) if (zap) { console.log(`Received ${zap.invoiceAmount} msat`, zap.request.content) } ``` ### NIP-42 relay authentication ```typescript import { makeRelayAuth } from '@welshman/util' // Inside relay AUTH handler const authEvent = makeRelayAuth('wss://relay.example.com', challengeFromRelay) const signed = await signer.sign(authEvent) // send signed AUTH message to relay ``` ### NIP-98 HTTP authentication ```typescript import { makeHttpAuth, makeHttpAuthHeader } from '@welshman/util' const body = JSON.stringify({ data: 'example' }) const authEvent = await makeHttpAuth('https://api.example.com/upload', 'POST', body) const signed = await signer.signEvent(authEvent) await fetch('https://api.example.com/upload', { method: 'POST', body, headers: { Authorization: makeHttpAuthHeader(signed), 'Content-Type': 'application/json', }, }) ``` --- ## Integration Notes - **`@welshman/net`** — uses `TrustedEvent`, `Filter`, `SignedEvent` from this package as the wire types for relay connections and subscriptions. - **`@welshman/store`** — provides Svelte stores over repositories built on `TrustedEvent`; relies on `isReplaceable`, `getAddress`, etc. for deduplication. - **`@welshman/app`** — high-level application layer; wraps net/store/router and uses profile, list, zap, and handler helpers from this package. - **`@welshman/router`** — uses `RelayMode` and relay URL helpers when computing relay selections. - **`@welshman/signer`** — produces `SignedEvent` objects that satisfy types defined here; the `Encrypt` function type used by `Encryptable` is typically provided by a signer. --- ## Gotchas & Tips - **`TrustedEvent` vs `SignedEvent`**: Most in-app code should accept `TrustedEvent` (has id, may have sig). Only require `SignedEvent` when you need to ensure the event has a signature. - **Replaceable event identity**: Use `getIdOrAddress` rather than `event.id` when referencing events that may be addressable — the address string is stable across updates, the id is not. - **`getAncestors` handles two protocols**: Kind 1111 (comment/NIP-22) uses uppercase `E`/`A` for roots and lowercase for replies, returning `{ roots, replies }`. All other kinds use NIP-10 positional rules, returning `{ roots, replies, mentions }` where `mentions` is always present but may be an empty array. You do not need to branch on this; `getAncestors`, `getParentIdOrAddr`, and `isChildOf` handle it automatically. - **List mutations return `Encryptable`**: `addToListPrivately`, `removeFromList`, etc. do not return an event directly. Call `.reconcile(encryptFn)` on the result to get the final `EventTemplate` ready to sign. - **`zapFromEvent` returns `undefined` on any validation failure** including amount mismatch, wrong zapper pubkey, malformed invoice, or self-zap. Always check the result. - **`getLnUrl` handles three input forms**: bare lightning address (`user@domain`), full HTTPS URL, or already-encoded `lnurl1...`. Returns `undefined` for anything else. - **`normalizeRelayUrl` vs `displayRelayUrl`**: Use `normalizeRelayUrl` before storing or comparing relay URLs. Use `displayRelayUrl` only for human-readable display (strips protocol/trailing slash). - **`Address.isAddress`** checks the `kind:pubkey:identifier` format only, not naddr. To validate an naddr string, use `Address.fromNaddr` inside a try/catch. - **`getTagValue` / `getTagValues` argument order**: the type(s) come **first**, the tags array comes **second** — `getTagValue('title', event.tags)`. This is the opposite of the specialized helpers like `getEventTags(tags)` which take only the tags array. Mixing up the order produces no TypeScript error but silently returns `undefined` or `[]`. - **`verifiedSymbol` is a Symbol key**: you must import `verifiedSymbol` from `@welshman/util` and use it as a computed property key — `event[verifiedSymbol] = true`. You cannot use a string key. The symbol is re-exported from `nostr-tools/pure`, so it is the same identity as the one used internally by `verifyEvent`.