diff --git a/src/lib/nostrIdentifier.ts b/src/lib/nostrIdentifier.ts
index bfac9253..0d5c186e 100644
--- a/src/lib/nostrIdentifier.ts
+++ b/src/lib/nostrIdentifier.ts
@@ -5,6 +5,11 @@ import { nip19 } from 'nostr-tools';
*/
const NIP19_PREFIXES = ['npub1', 'nprofile1', 'note1', 'nevent1', 'naddr1'];
+/**
+ * Matches a 64-character lowercase hex string (a raw Nostr event ID or pubkey).
+ */
+const HEX_64_RE = /^[0-9a-f]{64}$/;
+
/**
* Checks whether a string looks like a NIP-05 identifier (user@domain.com)
* or a bare domain (e.g. fiatjaf.com). Strips a leading `@` if present.
@@ -46,6 +51,11 @@ export function getNostrIdentifierPath(input: string): string | null {
}
}
+ // Try raw 64-char hex (event ID or pubkey) — route handles it directly
+ if (HEX_64_RE.test(value)) {
+ return `/${value}`;
+ }
+
// Try NIP-05
if (looksLikeNip05(value)) {
// Strip leading @ for the URL path
diff --git a/src/pages/NIP19Page.tsx b/src/pages/NIP19Page.tsx
index 210b5c99..a4ec483e 100644
--- a/src/pages/NIP19Page.tsx
+++ b/src/pages/NIP19Page.tsx
@@ -1,10 +1,14 @@
import { nip19 } from 'nostr-tools';
import { useParams } from 'react-router-dom';
+import { useNostr } from '@nostrify/react';
+import { useQuery } from '@tanstack/react-query';
import NotFound from './NotFound';
import { ProfilePage } from './ProfilePage';
import { PostDetailPage, AddrPostDetailPage } from './PostDetailPage';
import type { AddressPointer } from 'nostr-tools/nip19';
+const HEX_64_RE = /^[0-9a-f]{64}$/;
+
/**
* Returns true if the identifier looks like a NIP-05 username (contains @).
*/
@@ -12,12 +16,50 @@ function isNip05Like(id: string): boolean {
return id.includes('@');
}
+/**
+ * Resolves a raw 64-char hex string to either an event ID or a pubkey.
+ * Tries to find an event with that ID first; if none is found, checks
+ * whether a kind-0 profile exists for it as a pubkey.
+ */
+function HexIdentifierPage({ hex }: { hex: string }) {
+ const { nostr } = useNostr();
+
+ const { data: resolved, isLoading } = useQuery({
+ queryKey: ['hex-resolve', hex],
+ queryFn: async () => {
+ // Try as event ID first
+ const events = await nostr.query([{ ids: [hex], limit: 1 }]);
+ if (events.length > 0) {
+ return 'event' as const;
+ }
+ // Try as pubkey (look for kind 0 profile)
+ const profiles = await nostr.query([{ kinds: [0], authors: [hex], limit: 1 }]);
+ if (profiles.length > 0) {
+ return 'pubkey' as const;
+ }
+ return 'event' as const; // default to event — PostDetailPage will handle "not found"
+ },
+ staleTime: Infinity,
+ });
+
+ if (isLoading || !resolved) {
+ return null;
+ }
+
+ if (resolved === 'pubkey') {
+ return ;
+ }
+
+ return ;
+}
+
/**
* Universal route handler for `/:param`.
*
* Dispatches based on the shape of the identifier:
* - NIP-19 (`npub1...`, `note1...`, `nevent1...`, `naddr1...`, `nprofile1...`)
* - NIP-05 (`user@domain.com`) → profile
+ * - Raw 64-char hex → event ID or pubkey (resolved via relay query)
*/
export function NIP19Page() {
const { nip19: identifier } = useParams<{ nip19: string }>();
@@ -31,6 +73,11 @@ export function NIP19Page() {
return ;
}
+ // Raw 64-char hex — could be event ID or pubkey, need to resolve
+ if (HEX_64_RE.test(identifier)) {
+ return ;
+ }
+
// Try NIP-19 decoding
let decoded;
try {