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..b787170
--- /dev/null
+++ b/src/components/nostr/kinds/BadgeAwardRenderer.tsx
@@ -0,0 +1,106 @@
+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";
+
+/**
+ * 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 badge thumbnail, name, and number of recipients
+ */
+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 */}
+ {badgeImageUrl ? (
+

+ ) : (
+
+ )}
+
+ {/* Badge Info */}
+
+ {badgeEvent ? (
+
+ {displayTitle}
+
+ ) : (
+
+ {displayTitle}
+
+ )}
+
+
+ Awarded to {recipientCount}{" "}
+ {recipientCount === 1 ? "person" : "people"}
+
+
+
+
+ );
+}
diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx
index f3de435..5fabfc0 100644
--- a/src/components/nostr/kinds/index.tsx
+++ b/src/components/nostr/kinds/index.tsx
@@ -144,6 +144,8 @@ import { P2pOrderRenderer } from "./P2pOrderRenderer";
import { P2pOrderDetailRenderer } from "./P2pOrderDetailRenderer";
import { BadgeDefinitionRenderer } from "./BadgeDefinitionRenderer";
import { BadgeDefinitionDetailRenderer } from "./BadgeDefinitionDetailRenderer";
+import { BadgeAwardRenderer } from "./BadgeAwardRenderer";
+import { BadgeAwardDetailRenderer } from "./BadgeAwardDetailRenderer";
/**
* Registry of kind-specific renderers
@@ -155,6 +157,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
@@ -268,6 +271,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)
diff --git a/src/lib/nip58-helpers.ts b/src/lib/nip58-helpers.ts
index f89b5d7..1b36102 100644
--- a/src/lib/nip58-helpers.ts
+++ b/src/lib/nip58-helpers.ts
@@ -9,7 +9,7 @@ import type { NostrEvent } from "nostr-tools";
*/
/**
- * Get the unique identifier for a badge definition (d tag)
+ * Get the unique identifier for a badge (d tag)
*/
export function getBadgeIdentifier(event: NostrEvent): string | undefined {
return getTagValue(event, "d");
@@ -78,14 +78,14 @@ export function getBadgeImageUrl(event: NostrEvent): string | null {
/**
* Get all pubkeys awarded this badge (from kind 8 award events)
- * Note: This should be called on award events (kind 8), not badge definitions
+ * 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 definition address referenced by an award event (kind 8)
+ * Get the badge address referenced by an award event (kind 8)
* @returns The "a" tag value (e.g., "30009:pubkey:identifier")
*/
export function getAwardBadgeAddress(