diff --git a/package-lock.json b/package-lock.json
index abd2db3..6ef4afb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -50,6 +50,7 @@
"dexie-react-hooks": "^4.2.0",
"flexsearch": "^0.8.212",
"framer-motion": "^12.23.26",
+ "hash-sum": "^2.0.0",
"hls-video-element": "^1.5.10",
"hls.js": "^1.6.15",
"jotai": "^2.15.2",
diff --git a/package.json b/package.json
index 494749e..4a13e62 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"dexie-react-hooks": "^4.2.0",
"flexsearch": "^0.8.212",
"framer-motion": "^12.23.26",
+ "hash-sum": "^2.0.0",
"hls-video-element": "^1.5.10",
"hls.js": "^1.6.15",
"jotai": "^2.15.2",
diff --git a/src/components/nostr/kinds/BadgeAwardDetailRenderer.tsx b/src/components/nostr/kinds/BadgeAwardDetailRenderer.tsx
new file mode 100644
index 0000000..b6efdd0
--- /dev/null
+++ b/src/components/nostr/kinds/BadgeAwardDetailRenderer.tsx
@@ -0,0 +1,139 @@
+import { NostrEvent } from "@/types/nostr";
+import { getAwardBadgeAddress, getAwardedPubkeys } from "@/lib/nip58-helpers";
+import { use$ } from "applesauce-react/hooks";
+import eventStore from "@/services/event-store";
+import { AddressPointer } from "nostr-tools/nip19";
+import {
+ getBadgeName,
+ getBadgeIdentifier,
+ getBadgeImageUrl,
+} from "@/lib/nip58-helpers";
+import { UserName } from "../UserName";
+import { Award } from "lucide-react";
+import { ClickableEventTitle } from "./BaseEventRenderer";
+
+interface BadgeAwardDetailRendererProps {
+ event: NostrEvent;
+}
+
+/**
+ * Parse an address pointer from an a tag value
+ * Format: "kind:pubkey:identifier"
+ */
+function parseAddress(aTagValue: string): AddressPointer | null {
+ const parts = aTagValue.split(":");
+ if (parts.length !== 3) return null;
+
+ const kind = parseInt(parts[0], 10);
+ const pubkey = parts[1];
+ const identifier = parts[2];
+
+ if (isNaN(kind) || !pubkey || identifier === undefined) return null;
+
+ return { kind, pubkey, identifier };
+}
+
+/**
+ * Detail renderer for Kind 8 - Badge Award (NIP-58)
+ * Shows badge information and list of recipients
+ */
+export function BadgeAwardDetailRenderer({
+ event,
+}: BadgeAwardDetailRendererProps) {
+ const badgeAddress = getAwardBadgeAddress(event);
+ const awardedPubkeys = getAwardedPubkeys(event);
+
+ // Parse the badge address (30009:pubkey:identifier)
+ const coordinate = badgeAddress ? parseAddress(badgeAddress) : null;
+
+ // Fetch the badge event
+ const badgeEvent = use$(
+ () =>
+ coordinate
+ ? eventStore.replaceable(
+ coordinate.kind,
+ coordinate.pubkey,
+ coordinate.identifier,
+ )
+ : undefined,
+ [coordinate?.kind, coordinate?.pubkey, coordinate?.identifier],
+ );
+
+ // Get badge metadata
+ const badgeName = badgeEvent ? getBadgeName(badgeEvent) : null;
+ const badgeIdentifier = badgeEvent ? getBadgeIdentifier(badgeEvent) : null;
+ const badgeImageUrl = badgeEvent ? getBadgeImageUrl(badgeEvent) : null;
+
+ const displayTitle = badgeName || badgeIdentifier || "Badge";
+
+ return (
+
+ {/* Badge Header */}
+
+ {/* Badge Image */}
+ {badgeImageUrl ? (
+

+ ) : (
+
+ )}
+
+ {/* Badge Title */}
+
+ {badgeEvent ? (
+
+ {displayTitle}
+
+ ) : (
+
{displayTitle}
+ )}
+
Badge Award
+
+
+
+ {/* Metadata */}
+
+
Issued By
+
+
+
+ {/* Recipients List */}
+ {awardedPubkeys.length > 0 && (
+
+
+ Awarded to {awardedPubkeys.length}{" "}
+ {awardedPubkeys.length === 1 ? "Person" : "People"}
+
+
+ {awardedPubkeys.map((pubkey) => (
+
+ ))}
+
+
+ )}
+
+ {/* Award Comment */}
+ {event.content && (
+
+
Comment
+
{event.content}
+
+ )}
+
+ );
+}
diff --git a/src/components/nostr/kinds/BadgeAwardRenderer.tsx b/src/components/nostr/kinds/BadgeAwardRenderer.tsx
new file mode 100644
index 0000000..abef1fc
--- /dev/null
+++ b/src/components/nostr/kinds/BadgeAwardRenderer.tsx
@@ -0,0 +1,111 @@
+import {
+ BaseEventContainer,
+ BaseEventProps,
+ ClickableEventTitle,
+} from "./BaseEventRenderer";
+import { getAwardBadgeAddress, getAwardedPubkeys } from "@/lib/nip58-helpers";
+import { use$ } from "applesauce-react/hooks";
+import eventStore from "@/services/event-store";
+import { AddressPointer } from "nostr-tools/nip19";
+import {
+ getBadgeName,
+ getBadgeIdentifier,
+ getBadgeImageUrl,
+} from "@/lib/nip58-helpers";
+import { Award } from "lucide-react";
+import { UserName } from "../UserName";
+
+/**
+ * Parse an address pointer from an a tag value
+ * Format: "kind:pubkey:identifier"
+ */
+function parseAddress(aTagValue: string): AddressPointer | null {
+ const parts = aTagValue.split(":");
+ if (parts.length !== 3) return null;
+
+ const kind = parseInt(parts[0], 10);
+ const pubkey = parts[1];
+ const identifier = parts[2];
+
+ if (isNaN(kind) || !pubkey || identifier === undefined) return null;
+
+ return { kind, pubkey, identifier };
+}
+
+/**
+ * Renderer for Kind 8 - Badge Award (NIP-58)
+ * Shows inline badge thumbnail, name, and linked recipient count
+ */
+export function BadgeAwardRenderer({ event }: BaseEventProps) {
+ const badgeAddress = getAwardBadgeAddress(event);
+ const awardedPubkeys = getAwardedPubkeys(event);
+
+ // Parse the badge address (30009:pubkey:identifier)
+ const coordinate = badgeAddress ? parseAddress(badgeAddress) : null;
+
+ // Fetch the badge event
+ const badgeEvent = use$(
+ () =>
+ coordinate
+ ? eventStore.replaceable(
+ coordinate.kind,
+ coordinate.pubkey,
+ coordinate.identifier,
+ )
+ : undefined,
+ [coordinate?.kind, coordinate?.pubkey, coordinate?.identifier],
+ );
+
+ // Get badge metadata
+ const badgeName = badgeEvent ? getBadgeName(badgeEvent) : null;
+ const badgeIdentifier = badgeEvent ? getBadgeIdentifier(badgeEvent) : null;
+ const badgeImageUrl = badgeEvent ? getBadgeImageUrl(badgeEvent) : null;
+
+ const displayTitle = badgeName || badgeIdentifier || "Badge";
+ const recipientCount = awardedPubkeys.length;
+
+ return (
+
+
+ {/* Badge Thumbnail - small inline */}
+ {badgeImageUrl ? (
+

+ ) : (
+
+ )}
+
+ {/* Badge Name - linked to badge event */}
+ {badgeEvent ? (
+
+ {displayTitle}
+
+ ) : (
+
+ {displayTitle}
+
+ )}
+
+ {/* Awarded count/name - linked to this award event */}
+
+ awarded to
+ {recipientCount === 1 ? (
+
+ ) : (
+ {recipientCount} people
+ )}
+
+
+
+ );
+}
diff --git a/src/components/nostr/kinds/BadgeDefinitionDetailRenderer.tsx b/src/components/nostr/kinds/BadgeDefinitionDetailRenderer.tsx
new file mode 100644
index 0000000..e0b1ed6
--- /dev/null
+++ b/src/components/nostr/kinds/BadgeDefinitionDetailRenderer.tsx
@@ -0,0 +1,135 @@
+import { NostrEvent } from "@/types/nostr";
+import {
+ getBadgeIdentifier,
+ getBadgeName,
+ getBadgeDescription,
+ getBadgeImage,
+ getBadgeThumbnails,
+} from "@/lib/nip58-helpers";
+import { UserName } from "../UserName";
+import { Award } from "lucide-react";
+
+interface BadgeDefinitionDetailRendererProps {
+ event: NostrEvent;
+}
+
+/**
+ * Image variant display component
+ */
+function ImageVariant({
+ url,
+ dimensions,
+ label,
+}: {
+ url: string;
+ dimensions?: string;
+ label: string;
+}) {
+ return (
+
+
+
+ {label}
+
+ {dimensions && (
+ {dimensions}
+ )}
+
+

+
+ );
+}
+
+/**
+ * Detail renderer for Kind 30009 - Badge (NIP-58)
+ * Shows badge information including all image variants
+ */
+export function BadgeDefinitionDetailRenderer({
+ event,
+}: BadgeDefinitionDetailRendererProps) {
+ const identifier = getBadgeIdentifier(event);
+ const name = getBadgeName(event);
+ const description = getBadgeDescription(event);
+ const image = getBadgeImage(event);
+ const thumbnails = getBadgeThumbnails(event);
+
+ // Use name if available, fallback to identifier
+ const displayTitle = name || identifier || "Badge";
+
+ return (
+
+ {/* Header Section */}
+
+ {/* Badge Image */}
+ {image ? (
+

+ ) : (
+
+ )}
+
+ {/* Badge Title & Description */}
+
+
{displayTitle}
+ {description && (
+
{description}
+ )}
+
+
+
+ {/* Metadata Grid */}
+
+ {/* Issuer */}
+
+
Issued By
+
+
+
+ {/* Identifier */}
+ {identifier && (
+
+
Badge ID
+
+ {identifier}
+
+
+ )}
+
+
+ {/* Image Variants Section */}
+ {(image || thumbnails.length > 0) && (
+
+
Image Variants
+
+ {image && (
+
+ )}
+ {thumbnails.map((thumb, idx) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/nostr/kinds/BadgeDefinitionRenderer.tsx b/src/components/nostr/kinds/BadgeDefinitionRenderer.tsx
new file mode 100644
index 0000000..7b29420
--- /dev/null
+++ b/src/components/nostr/kinds/BadgeDefinitionRenderer.tsx
@@ -0,0 +1,42 @@
+import {
+ BaseEventContainer,
+ BaseEventProps,
+ ClickableEventTitle,
+} from "./BaseEventRenderer";
+import {
+ getBadgeIdentifier,
+ getBadgeName,
+ getBadgeDescription,
+} from "@/lib/nip58-helpers";
+
+/**
+ * Renderer for Kind 30009 - Badge (NIP-58)
+ * Simple feed view with name and description
+ */
+export function BadgeDefinitionRenderer({ event }: BaseEventProps) {
+ const identifier = getBadgeIdentifier(event);
+ const name = getBadgeName(event);
+ const description = getBadgeDescription(event);
+
+ // Use name if available, fallback to identifier
+ const displayTitle = name || identifier || "Badge";
+
+ return (
+
+
+
+ {displayTitle}
+
+
+ {description && (
+
+ {description}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/nostr/kinds/ProfileBadgesDetailRenderer.tsx b/src/components/nostr/kinds/ProfileBadgesDetailRenderer.tsx
new file mode 100644
index 0000000..1acf0ca
--- /dev/null
+++ b/src/components/nostr/kinds/ProfileBadgesDetailRenderer.tsx
@@ -0,0 +1,162 @@
+import { NostrEvent } from "@/types/nostr";
+import { getProfileBadgePairs } from "@/lib/nip58-helpers";
+import { use$ } from "applesauce-react/hooks";
+import eventStore from "@/services/event-store";
+import { AddressPointer } from "nostr-tools/nip19";
+import {
+ getBadgeName,
+ getBadgeIdentifier,
+ getBadgeDescription,
+ getBadgeImageUrl,
+} from "@/lib/nip58-helpers";
+import { Award } from "lucide-react";
+import { UserName } from "../UserName";
+import { ClickableEventTitle } from "./BaseEventRenderer";
+
+interface ProfileBadgesDetailRendererProps {
+ event: NostrEvent;
+}
+
+/**
+ * Parse an address pointer from an a tag value
+ * Format: "kind:pubkey:identifier"
+ */
+function parseAddress(aTagValue: string): AddressPointer | null {
+ const parts = aTagValue.split(":");
+ if (parts.length !== 3) return null;
+
+ const kind = parseInt(parts[0], 10);
+ const pubkey = parts[1];
+ const identifier = parts[2];
+
+ if (isNaN(kind) || !pubkey || identifier === undefined) return null;
+
+ return { kind, pubkey, identifier };
+}
+
+/**
+ * Single badge row component with author, image, name, and description
+ */
+function BadgeRow({
+ badgeAddress,
+ awardEventId,
+}: {
+ badgeAddress: string;
+ awardEventId: string;
+}) {
+ const coordinate = parseAddress(badgeAddress);
+
+ // Fetch the badge definition event
+ const badgeEvent = use$(
+ () =>
+ coordinate
+ ? eventStore.replaceable(
+ coordinate.kind,
+ coordinate.pubkey,
+ coordinate.identifier,
+ )
+ : undefined,
+ [coordinate?.kind, coordinate?.pubkey, coordinate?.identifier],
+ );
+
+ // Fetch the award event
+ const awardEvent = use$(() => eventStore.event(awardEventId), [awardEventId]);
+
+ const badgeName = badgeEvent ? getBadgeName(badgeEvent) : null;
+ const badgeIdentifier = badgeEvent ? getBadgeIdentifier(badgeEvent) : null;
+ const badgeDescription = badgeEvent ? getBadgeDescription(badgeEvent) : null;
+ const badgeImageUrl = badgeEvent ? getBadgeImageUrl(badgeEvent) : null;
+
+ const displayTitle = badgeName || badgeIdentifier || "Badge";
+
+ return (
+
+ {/* Badge Image */}
+ {badgeImageUrl ? (
+

+ ) : (
+
+ )}
+
+ {/* Badge Info */}
+
+ {/* Issuer */}
+ {awardEvent && (
+
+
+
+ )}
+
+ {/* Badge Name */}
+ {badgeEvent ? (
+
+ {displayTitle}
+
+ ) : (
+
+ {displayTitle}
+
+ )}
+
+ {/* Badge Description */}
+ {badgeDescription && (
+
{badgeDescription}
+ )}
+
+
+ );
+}
+
+/**
+ * Detail renderer for Kind 30008 - Profile Badges (NIP-58)
+ * Shows all badges in a vertical list
+ */
+export function ProfileBadgesDetailRenderer({
+ event,
+}: ProfileBadgesDetailRendererProps) {
+ const badgePairs = getProfileBadgePairs(event);
+
+ return (
+
+ {/* Header */}
+
+
Profile Badges
+
+
+ •
+
+ {badgePairs.length} {badgePairs.length === 1 ? "badge" : "badges"}
+
+
+
+
+ {/* Badges List */}
+ {badgePairs.length > 0 ? (
+
+ {badgePairs.map((pair, idx) => (
+
+ ))}
+
+ ) : (
+
+
+
No badges to display
+
+ )}
+
+ );
+}
diff --git a/src/components/nostr/kinds/ProfileBadgesRenderer.tsx b/src/components/nostr/kinds/ProfileBadgesRenderer.tsx
new file mode 100644
index 0000000..f1ff6cf
--- /dev/null
+++ b/src/components/nostr/kinds/ProfileBadgesRenderer.tsx
@@ -0,0 +1,113 @@
+import {
+ BaseEventContainer,
+ BaseEventProps,
+ ClickableEventTitle,
+} from "./BaseEventRenderer";
+import { getProfileBadgePairs } from "@/lib/nip58-helpers";
+import { use$ } from "applesauce-react/hooks";
+import eventStore from "@/services/event-store";
+import { AddressPointer } from "nostr-tools/nip19";
+import {
+ getBadgeName,
+ getBadgeIdentifier,
+ getBadgeImageUrl,
+} from "@/lib/nip58-helpers";
+import { Award } from "lucide-react";
+
+/**
+ * Parse an address pointer from an a tag value
+ * Format: "kind:pubkey:identifier"
+ */
+function parseAddress(aTagValue: string): AddressPointer | null {
+ const parts = aTagValue.split(":");
+ if (parts.length !== 3) return null;
+
+ const kind = parseInt(parts[0], 10);
+ const pubkey = parts[1];
+ const identifier = parts[2];
+
+ if (isNaN(kind) || !pubkey || identifier === undefined) return null;
+
+ return { kind, pubkey, identifier };
+}
+
+/**
+ * Single badge display component for feed view
+ */
+function BadgeItem({ badgeAddress }: { badgeAddress: string }) {
+ const coordinate = parseAddress(badgeAddress);
+
+ // Fetch the badge event
+ const badgeEvent = use$(
+ () =>
+ coordinate
+ ? eventStore.replaceable(
+ coordinate.kind,
+ coordinate.pubkey,
+ coordinate.identifier,
+ )
+ : undefined,
+ [coordinate?.kind, coordinate?.pubkey, coordinate?.identifier],
+ );
+
+ const badgeName = badgeEvent ? getBadgeName(badgeEvent) : null;
+ const badgeIdentifier = badgeEvent ? getBadgeIdentifier(badgeEvent) : null;
+ const badgeImageUrl = badgeEvent ? getBadgeImageUrl(badgeEvent) : null;
+
+ const displayTitle = badgeName || badgeIdentifier || "Badge";
+
+ return (
+
+ {badgeImageUrl ? (
+

+ ) : (
+
+ )}
+
+ );
+}
+
+/**
+ * Renderer for Kind 30008 - Profile Badges (NIP-58)
+ * Shows all badge thumbnails, clickable to open detail view
+ */
+export function ProfileBadgesRenderer({ event }: BaseEventProps) {
+ const badgePairs = getProfileBadgePairs(event);
+
+ if (badgePairs.length === 0) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Badge Count - Clickable Title */}
+
+ {badgePairs.length} {badgePairs.length === 1 ? "badge" : "badges"}
+
+
+ {/* All Badge Thumbnails */}
+
+ {badgePairs.map((pair, idx) => (
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx
index 59b4b05..1a8396e 100644
--- a/src/components/nostr/kinds/index.tsx
+++ b/src/components/nostr/kinds/index.tsx
@@ -142,6 +142,12 @@ import { NostrEvent } from "@/types/nostr";
import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer";
import { P2pOrderRenderer } from "./P2pOrderRenderer";
import { P2pOrderDetailRenderer } from "./P2pOrderDetailRenderer";
+import { BadgeDefinitionRenderer } from "./BadgeDefinitionRenderer";
+import { BadgeDefinitionDetailRenderer } from "./BadgeDefinitionDetailRenderer";
+import { BadgeAwardRenderer } from "./BadgeAwardRenderer";
+import { BadgeAwardDetailRenderer } from "./BadgeAwardDetailRenderer";
+import { ProfileBadgesRenderer } from "./ProfileBadgesRenderer";
+import { ProfileBadgesDetailRenderer } from "./ProfileBadgesDetailRenderer";
/**
* Registry of kind-specific renderers
@@ -153,6 +159,7 @@ const kindRenderers: Record> = {
3: Kind3Renderer, // Contact List
6: RepostRenderer, // Repost
7: Kind7Renderer, // Reaction
+ 8: BadgeAwardRenderer, // Badge Award (NIP-58)
9: Kind9Renderer, // Chat Message (NIP-C7)
11: Kind1Renderer, // Public Thread Reply (NIP-10)
16: RepostRenderer, // Generic Repost
@@ -200,6 +207,8 @@ const kindRenderers: Record> = {
30005: VideoCurationSetRenderer, // Video Curation Sets (NIP-51)
30006: PictureCurationSetRenderer, // Picture Curation Sets (NIP-51)
30007: KindMuteSetRenderer, // Kind Mute Sets (NIP-51)
+ 30008: ProfileBadgesRenderer, // Profile Badges (NIP-58)
+ 30009: BadgeDefinitionRenderer, // Badge (NIP-58)
30015: InterestSetRenderer, // Interest Sets (NIP-51)
30023: Kind30023Renderer, // Long-form Article
30030: EmojiSetRenderer, // Emoji Sets (NIP-30)
@@ -265,6 +274,7 @@ const detailRenderers: Record<
> = {
0: Kind0DetailRenderer, // Profile Metadata Detail
3: Kind3DetailView, // Contact List Detail
+ 8: BadgeAwardDetailRenderer, // Badge Award Detail (NIP-58)
777: SpellDetailRenderer, // Spell Detail
1337: Kind1337DetailRenderer, // Code Snippet Detail (NIP-C0)
1617: PatchDetailRenderer, // Patch Detail (NIP-34)
@@ -293,6 +303,8 @@ const detailRenderers: Record<
30005: VideoCurationSetDetailRenderer, // Video Curation Sets Detail (NIP-51)
30006: PictureCurationSetDetailRenderer, // Picture Curation Sets Detail (NIP-51)
30007: KindMuteSetDetailRenderer, // Kind Mute Sets Detail (NIP-51)
+ 30008: ProfileBadgesDetailRenderer, // Profile Badges Detail (NIP-58)
+ 30009: BadgeDefinitionDetailRenderer, // Badge Detail (NIP-58)
30015: InterestSetDetailRenderer, // Interest Sets Detail (NIP-51)
30023: Kind30023DetailRenderer, // Long-form Article Detail
30030: EmojiSetDetailRenderer, // Emoji Sets Detail (NIP-30)
diff --git a/src/lib/nip58-helpers.ts b/src/lib/nip58-helpers.ts
new file mode 100644
index 0000000..e2bc4d5
--- /dev/null
+++ b/src/lib/nip58-helpers.ts
@@ -0,0 +1,125 @@
+import { getTagValue } from "applesauce-core/helpers";
+import { getTagValues } from "./nostr-utils";
+import type { NostrEvent } from "nostr-tools";
+
+/**
+ * NIP-58 Badge Helpers
+ * These helpers extract badge-related metadata from badge events.
+ * They wrap getTagValue which caches results internally, so no need for useMemo.
+ */
+
+/**
+ * Get the unique identifier for a badge (d tag)
+ */
+export function getBadgeIdentifier(event: NostrEvent): string | undefined {
+ return getTagValue(event, "d");
+}
+
+/**
+ * Get the display name for a badge
+ */
+export function getBadgeName(event: NostrEvent): string | undefined {
+ return getTagValue(event, "name");
+}
+
+/**
+ * Get the description explaining the badge meaning or issuance criteria
+ */
+export function getBadgeDescription(event: NostrEvent): string | undefined {
+ return getTagValue(event, "description");
+}
+
+/**
+ * Get the badge image URL and optional dimensions
+ * @returns Object with url and optional dimensions (e.g., "1024x1024")
+ */
+export function getBadgeImage(event: NostrEvent): {
+ url: string;
+ dimensions?: string;
+} | null {
+ const imageTag = event.tags.find((tag) => tag[0] === "image" && tag[1]);
+ if (!imageTag) return null;
+
+ return {
+ url: imageTag[1],
+ dimensions: imageTag[2],
+ };
+}
+
+/**
+ * Get all thumbnail variants with dimensions
+ * @returns Array of thumbnails with url and optional dimensions
+ */
+export function getBadgeThumbnails(event: NostrEvent): Array<{
+ url: string;
+ dimensions?: string;
+}> {
+ return event.tags
+ .filter((tag) => tag[0] === "thumb" && tag[1])
+ .map((tag) => ({
+ url: tag[1],
+ dimensions: tag[2],
+ }));
+}
+
+/**
+ * Get the best badge image URL to display based on available variants
+ * Prefers image over thumbnails
+ */
+export function getBadgeImageUrl(event: NostrEvent): string | null {
+ const image = getBadgeImage(event);
+ if (image) return image.url;
+
+ const thumbnails = getBadgeThumbnails(event);
+ if (thumbnails.length > 0) return thumbnails[0].url;
+
+ return null;
+}
+
+/**
+ * Get all pubkeys awarded this badge (from kind 8 award events)
+ * Note: This should be called on award events (kind 8), not badges (kind 30009)
+ */
+export function getAwardedPubkeys(awardEvent: NostrEvent): string[] {
+ return getTagValues(awardEvent, "p");
+}
+
+/**
+ * Get the badge address referenced by an award event (kind 8)
+ * @returns The "a" tag value (e.g., "30009:pubkey:identifier")
+ */
+export function getAwardBadgeAddress(
+ awardEvent: NostrEvent,
+): string | undefined {
+ return getTagValue(awardEvent, "a");
+}
+
+/**
+ * Badge pair from Profile Badges event (kind 30008)
+ * Contains references to both the badge definition and the award event
+ */
+export interface BadgePair {
+ badgeAddress: string; // a tag - references badge definition (30009:pubkey:identifier)
+ awardEventId: string; // e tag - references award event (kind 8)
+}
+
+/**
+ * Extract ordered badge pairs from Profile Badges event (kind 30008)
+ * Returns pairs of (badge definition address, award event id)
+ */
+export function getProfileBadgePairs(event: NostrEvent): BadgePair[] {
+ const pairs: BadgePair[] = [];
+ const aTags = event.tags.filter((tag) => tag[0] === "a" && tag[1]);
+ const eTags = event.tags.filter((tag) => tag[0] === "e" && tag[1]);
+
+ // Pair them up in order - each a tag should have a corresponding e tag
+ const minLength = Math.min(aTags.length, eTags.length);
+ for (let i = 0; i < minLength; i++) {
+ pairs.push({
+ badgeAddress: aTags[i][1],
+ awardEventId: eTags[i][1],
+ });
+ }
+
+ return pairs;
+}