Files
welshman/skills/welshman-util/SKILL.md
T
2026-06-10 14:01:08 -07:00

25 KiB
Raw Blame History


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

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 (2000029999)
isReplaceable(event) True for plain or parameterized replaceable
isPlainReplaceable(event) True for kinds 1000019999 and metadata/contacts
isParameterizedReplaceable(event) True for kinds 3000039999
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 1000010099)

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 3000030102)

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 50007000)

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 (50007000).

Kind classifiers

isRegularKind(kind)                // 10009999 and select low kinds
isPlainReplaceableKind(kind)       // 0, 3, and 1000019999
isEphemeralKind(kind)              // 2000029999
isParameterizedReplaceableKind(kind) // 3000039999
isReplaceableKind(kind)            // plain OR parameterized replaceable
isDVMKind(kind)                    // 50007000

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
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 — 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 secondgetTagValue('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.