25 KiB
name, description
| name | description |
|---|---|
| welshman-util | 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
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
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<T> |
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 |
isWebLNWallet(wallet) |
Type guard |
isNWCWallet(wallet) |
Type guard |
NIP-42 (Relay Auth)
makeRelayAuth(url: string, challenge: string): StampedEvent
// Creates kind 22242 auth event; sign before sending
NIP-98 (HTTP Auth)
makeHttpAuth(url: string, method?: string, body?: string): Promise<StampedEvent>
makeHttpAuthHeader(event: SignedEvent): string // Returns "Nostr <base64>"
NIP-86 (Relay Management)
sendManagementRequest(url: string, request: ManagementRequest, authEvent: SignedEvent): Promise<ManagementResponse>
// ManagementResponse = { result?: any; error?: string }
// ManagementMethod enum covers: BanPubkey, AllowPubkey, BanEvent, AllowEvent, etc.
Handlers (NIP-89)
readHandlers(event: TrustedEvent): Handler[]
getHandlerKey(handler: Handler): string // "kind:address" format
getHandlerAddress(event: TrustedEvent): string | undefined
displayHandler(handler?: Handler, fallback?: string): string
Links
fromNostrURI(s: string): string // strips "nostr:" or "nostr://" prefix
toNostrURI(s: string): string // ensures "nostr:" prefix
Blossom (Media Servers)
makeBlossomAuthEvent(opts: BlossomAuthEventOpts): StampedEvent
uploadBlob(server, blob, opts?): Promise<Response>
getBlob(server, sha256, opts?): Promise<Response>
deleteBlob(server, sha256, opts?): Promise<Response>
listBlobs(server, pubkey, opts?): Promise<Response>
checkBlobExists(server, sha256, opts?): Promise<{exists, size?}>
buildBlobUrl(server, sha256, extension?): string
encryptFile(file: Blob): Promise<EncryptedFile>
decryptFile(encryptedFile: EncryptedFile): Promise<Uint8Array>
Common Patterns
Creating and inspecting events
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:
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
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
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
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
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
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
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
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— usesTrustedEvent,Filter,SignedEventfrom this package as the wire types for relay connections and subscriptions.@welshman/store— provides Svelte stores over repositories built onTrustedEvent; relies onisReplaceable,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— usesRelayModeand relay URL helpers when computing relay selections.@welshman/signer— producesSignedEventobjects that satisfy types defined here; theEncryptfunction type used byEncryptableis typically provided by a signer.
Gotchas & Tips
-
TrustedEventvsSignedEvent: Most in-app code should acceptTrustedEvent(has id, may have sig). Only requireSignedEventwhen you need to ensure the event has a signature. -
Replaceable event identity: Use
getIdOrAddressrather thanevent.idwhen referencing events that may be addressable — the address string is stable across updates, the id is not. -
getAncestorshandles two protocols: Kind 1111 (comment/NIP-22) uses uppercaseE/Afor roots and lowercase for replies, returning{ roots, replies }. All other kinds use NIP-10 positional rules, returning{ roots, replies, mentions }wherementionsis always present but may be an empty array. You do not need to branch on this;getAncestors,getParentIdOrAddr, andisChildOfhandle it automatically. -
List mutations return
Encryptable:addToListPrivately,removeFromList, etc. do not return an event directly. Call.reconcile(encryptFn)on the result to get the finalEventTemplateready to sign. -
zapFromEventreturnsundefinedon any validation failure including amount mismatch, wrong zapper pubkey, malformed invoice, or self-zap. Always check the result. -
getLnUrlhandles three input forms: bare lightning address (user@domain), full HTTPS URL, or already-encodedlnurl1.... Returnsundefinedfor anything else. -
normalizeRelayUrlvsdisplayRelayUrl: UsenormalizeRelayUrlbefore storing or comparing relay URLs. UsedisplayRelayUrlonly for human-readable display (strips protocol/trailing slash). -
Address.isAddresschecks thekind:pubkey:identifierformat only, not naddr. To validate an naddr string, useAddress.fromNaddrinside a try/catch. -
getTagValue/getTagValuesargument order: the type(s) come first, the tags array comes second —getTagValue('title', event.tags). This is the opposite of the specialized helpers likegetEventTags(tags)which take only the tags array. Mixing up the order produces no TypeScript error but silently returnsundefinedor[]. -
verifiedSymbolis a Symbol key: you must importverifiedSymbolfrom@welshman/utiland use it as a computed property key —event[verifiedSymbol] = true. You cannot use a string key. The symbol is re-exported fromnostr-tools/pure, so it is the same identity as the one used internally byverifyEvent.