diff --git a/client/.roo/rules-code/rules.md b/client/.roo/rules-code/rules.md
new file mode 100644
index 0000000..7531fe5
--- /dev/null
+++ b/client/.roo/rules-code/rules.md
@@ -0,0 +1 @@
+Don't use "any" in typescript, find (or create) the propert types
\ No newline at end of file
diff --git a/client/1120_client.html b/client/1120_client.html
new file mode 100644
index 0000000..3050cb8
--- /dev/null
+++ b/client/1120_client.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <!-- Implement strict Content Security Policy -->
+    <title>HTTP Messages - CLIENT</title>
+    <!-- Load our CSS file -->
+    <link rel="stylesheet" href="./styles.css">
+</head>
+<body>
+    <!-- Navigation bar container - content will be injected by navbar.ts -->
+    <div id="navbarContainer" class="top-nav">
+        <!-- Navbar content will be injected here -->
+    </div>
+    
+    <!-- Main Content (HTTP Request Converter) -->
+    <div class="content">
+        <div class="info-box">
+            <p>This tool converts HTTP requests into a single Nostr event of kind 21120. The HTTP request is encrypted using NIP-44 in the content field with appropriate tags following the specification.</p>
+        </div>
+        
+        <h2>Client Information:</h2>
+        <div class="client-info" style="margin-bottom: 15px;">
+            <div style="display: flex; align-items: center; margin-bottom: 10px;">
+                <div style="font-weight: bold; margin-right: 10px;">Your Pubkey:</div>
+                <div id="clientPubkey" style="font-family: monospace; word-break: break-all;">Not logged in</div>
+                <button id="toggleClientKeyFormatBtn" class="server-format-button" title="Toggle format" style="margin-left: 10px;">HEX</button>
+            </div>
+            <div class="format-indicator">
+                <small id="clientKeyFormatIndicator">Currently showing: NPUB format</small>
+            </div>
+        </div>
+        
+        <h2>Server Information:</h2>
+        <div style="margin-bottom: 15px;">
+            <div style="margin-bottom: 10px;">
+                <label for="relay">Relay to search for servers:</label><br>
+                <div class="server-input-container">
+                    <input type="text" id="relay" value="wss://relay.degmods.com" class="server-input" style="border-radius: 4px 0 0 4px;">
+                    <button id="searchRelayBtn" class="server-search-button">Search Relay</button>
+                    <button id="refreshRelayBtn" class="server-refresh-button" title="Clear cache and fetch fresh data">🔄</button>
+                </div>
+            </div>
+            
+            <div style="margin-bottom: 10px;">
+                <label for="serverSelection">Choose a server:</label><br>
+                <div class="server-input-container">
+                    <input type="text" id="serverPubkey" placeholder="Selected server pubkey (d-tag) or enter manually" class="server-input">
+                    <button id="toggleKeyFormatBtn" class="server-format-button" title="Toggle format">HEX</button>
+                    <button id="selectServerBtn" class="server-select-button">Select Server</button>
+                </div>
+                <div class="format-indicator">
+                    <small id="keyFormatIndicator">Currently showing: NPUB format</small>
+                </div>
+                <div id="serverSelectionContainer" class="server-selection-container" style="display: none;">
+                    <div class="selection-header">
+                        <input type="text" id="serverSearchInput" placeholder="Search by operator name/pubkey" class="server-search-input">
+                        <button id="closeSelectionBtn" class="close-selection-btn">&times;</button>
+                    </div>
+                    <div id="serverList" class="server-list">
+                        <!-- Server list will be populated here -->
+                        <div class="server-list-loading">Loading servers...</div>
+                    </div>
+                </div>
+            </div>
+            
+            <div>
+                <label for="relay">Response Relay (optional):</label><br>
+                <input type="text" id="relay" value="wss://relay.degmods.com" style="width: 100%; padding: 8px;">
+            </div>
+        </div>
+        
+        <h2>Enter HTTP Request:</h2>
+        <textarea id="httpRequest" placeholder="GET /index.html HTTP/1.1
+Host: example.com
+User-Agent: Browser/1.0
+
+"></textarea>
+        
+        <button id="convertButton">Convert to Event</button>
+        
+        <div id="output" hidden>
+            <h2>Converted Event:</h2>
+            <div class="event-output-container">
+                <pre id="eventOutput"></pre>
+                <button id="copyEventButton" class="copy-button" title="Copy to clipboard">
+                    <span>Copy</span>
+                </button>
+            </div>
+            
+            <div class="publish-container">
+                <h2>Publish to Relay:</h2>
+                <div class="publish-input-container">
+                    <input type="text" id="publishRelay" value="wss://relay.degmods.com" placeholder="wss://relay.example.com" class="publish-input">
+                    <button id="publishButton" class="publish-button">Publish Event</button>
+                </div>
+                <div id="publishResult" class="publish-result" style="display: none;">
+                    <!-- Publish results will be shown here -->
+                </div>
+            </div>
+            
+            <div class="qr-container">
+                <h2>QR Code:</h2>
+                <div id="qrCode"></div>
+                <p><small>Scan this QR code to share the Nostr event</small></p>
+            </div>
+        </div>
+    </div>
+    <!-- Include the webpack bundled JavaScript file with forced loading -->
+    <script src="./main.bundle.js" onload="console.log('Main bundle loaded successfully')"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/client/receive.html b/client/1120_server.html
similarity index 60%
rename from client/receive.html
rename to client/1120_server.html
index a695806..36e8a5e 100644
--- a/client/receive.html
+++ b/client/1120_server.html
@@ -5,23 +5,13 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>HTTP Messages - SERVER</title>
     <link rel="stylesheet" href="./styles.css">
-    <script defer src="./bundle.js"></script>
+    <script defer src="./main.bundle.js"></script>
+    <!-- Additional chunks will be loaded automatically -->
 </head>
 <body>
-    <!-- Top Navigation Bar -->
-    <div class="top-nav">
-        <div class="nav-left">
-            <a href="./index.html" class="nav-link">CLIENT</a>
-            <a href="./receive.html" class="nav-link active">SERVER</a>
-            <a href="./billboard.html" class="nav-link">BILLBOARD</a>
-        </div>
-        <div class="nav-right">
-            <a href="./help.html" class="nav-link nav-icon" title="Documentation">❓</a>
-            <a href="./profile.html" class="nav-link nav-icon" title="Profile">👤</a>
-            <button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Dark Mode">
-                <span id="themeIcon">🌙</span>
-            </button>
-        </div>
+    <!-- Navigation bar container - content will be injected by navbar.ts -->
+    <div id="navbarContainer" class="top-nav">
+        <!-- Navbar content will be injected here -->
     </div>
     
     <!-- Main Content -->
@@ -29,6 +19,27 @@
         <div class="info-box">
             <p>This tool allows you to receive and view HTTP events (kind 21120) from Nostr relays, QR codes, or raw text input. Choose your preferred method using the tabs below.</p>
         </div>
+
+        <!-- Server Info Section - Now placed above the tabs -->
+        <h2>Server Information</h2>
+        <div class="server-section">
+            <div class="server-info-container">
+                <div class="server-npub-container">
+                    <label>Server Key:</label>
+                    <input type="text" id="serverNpub" readonly class="server-npub-input">
+                    <button id="toggleFormatBtn" class="toggle-format-btn" title="Toggle format">
+                        <span id="formatBtnText">HEX</span>
+                    </button>
+                    <button id="copyServerNpubBtn" class="copy-btn" title="Copy Key">
+                        <span id="copyBtnText">Copy</span>
+                    </button>
+                </div>
+                <div class="format-indicator">
+                    <small id="formatIndicator">Currently showing: NPUB format</small>
+                </div>
+            </div>
+            <div id="relayStatus" class="relay-status">Not connected</div>
+        </div>
         
         <!-- Tab Navigation - Updated data-tab attributes to match section IDs -->
         <div class="tabs">
@@ -42,22 +53,6 @@
         <div id="relay-connection-section" class="active">
             <h2>Relay Connection</h2>
             <div class="relay-connection">
-                <!-- Server npub display -->
-                <div class="server-info-container">
-                    <div class="server-npub-container">
-                        <label>Server Key:</label>
-                        <input type="text" id="serverNpub" readonly class="server-npub-input">
-                        <button id="toggleFormatBtn" class="toggle-format-btn" title="Toggle format">
-                            <span id="formatBtnText">HEX</span>
-                        </button>
-                        <button id="copyServerNpubBtn" class="copy-btn" title="Copy Key">
-                            <span id="copyBtnText">Copy</span>
-                        </button>
-                    </div>
-                    <div class="format-indicator">
-                        <small id="formatIndicator">Currently showing: NPUB format</small>
-                    </div>
-                </div>
                 
                 <div class="relay-input-container">
                     <label for="relayUrl">Relay URL:</label>
@@ -66,11 +61,10 @@
                 </div>
                 <div class="filter-options">
                     <label class="filter-checkbox">
-                        <input type="checkbox" id="showAllEvents" checked>
+                        <input type="checkbox" id="showAllEvents">
                         Show all kind 21120 events
                     </label>
                 </div>
-                <div id="relayStatus" class="relay-status">Not connected</div>
             </div>
         </div>
         
@@ -120,11 +114,35 @@
                         </div>
                         <!-- Selected event details will be shown here -->
                     </div>
+                    
+                    <!-- HTTP Response Modal -->
+                    <div id="httpResponseModal" class="http-response-modal" style="display: none;">
+                        <div class="http-response-container">
+                            <div class="http-response-header">
+                                <h3>HTTP Response</h3>
+                                <button class="close-modal-btn">&times;</button>
+                            </div>
+                            <div class="http-response-tabs">
+                                <button class="tab-btn active" data-tab="formatted-response">Formatted</button>
+                                <button class="tab-btn" data-tab="raw-response">Raw</button>
+                            </div>
+                            <div class="http-response-content">
+                                <div class="tab-content active" id="formatted-response">
+                                    <div class="http-formatted-container">
+                                        <!-- Formatted HTTP response will be shown here -->
+                                    </div>
+                                </div>
+                                <div class="tab-content" id="raw-response">
+                                    <pre><!-- Raw HTTP response will be shown here --></pre>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
                 </div>
             </div>
         </div>
     </div>
     
-    <!-- Script will be provided by bundle.js -->
+    <!-- Scripts are now loaded as chunks by webpack -->
 </body>
-</html>
+</html>
\ No newline at end of file
diff --git a/client/billboard.html b/client/billboard.html
index 8a04b06..c8b3af1 100644
--- a/client/billboard.html
+++ b/client/billboard.html
@@ -5,23 +5,13 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>HTTP Messages - BILLBOARD</title>
     <link rel="stylesheet" href="./styles.css">
-    <script defer src="./bundle.js"></script>
+    <script defer src="./main.bundle.js"></script>
+    <!-- Additional chunks will be loaded automatically -->
 </head>
 <body>
-    <!-- Top Navigation Bar -->
-    <div class="top-nav">
-        <div class="nav-left">
-            <a href="./index.html" class="nav-link">CLIENT</a>
-            <a href="./receive.html" class="nav-link">SERVER</a>
-            <a href="./billboard.html" class="nav-link active">BILLBOARD</a>
-        </div>
-        <div class="nav-right">
-            <a href="./help.html" class="nav-link nav-icon" title="Documentation">❓</a>
-            <a href="./profile.html" class="nav-link nav-icon" title="Profile">👤</a>
-            <button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Dark Mode">
-                <span id="themeIcon">🌙</span>
-            </button>
-        </div>
+    <!-- Navigation bar container - content will be injected by navbar.ts -->
+    <div id="navbarContainer" class="top-nav">
+        <!-- Navbar content will be injected here -->
     </div>
     
     <!-- Main Content -->
@@ -39,11 +29,17 @@
                     <input type="text" id="billboardRelayUrl" value="wss://relay.degmods.com" placeholder="wss://relay.example.com">
                     <button id="billboardConnectBtn" class="relay-connect-button">Connect</button>
                 </div>
+                <div class="filter-options">
+                    <label class="filter-checkbox">
+                        <input type="checkbox" id="showAllServerEvents">
+                        Show all kind 31120 events
+                    </label>
+                </div>
                 <div id="billboardRelayStatus" class="relay-status">Not connected</div>
             </div>
             
             <div class="billboard-actions">
-                <button id="createBillboardBtn" class="primary-button">Create New Billboard</button>
+                <button id="createBillboardBtn" class="primary-button">+ Add New Billboard</button>
             </div>
 
             <div class="billboard-container">
@@ -62,27 +58,58 @@
                         <span class="close-modal">&times;</span>
                     </div>
                     <div class="modal-body">
+                        <p class="form-hint">
+                            A billboard is a server registration event that allows clients to discover your HTTP-over-Nostr server.
+                        </p>
+                        
                         <form id="billboardForm">
                             <input type="hidden" id="editEventId" value="">
                             
-                            <div class="form-group">
-                                <label for="billboardDescription">Description:</label>
-                                <input type="text" id="billboardDescription" placeholder="HTTP-over-Nostr server">
-                            </div>
-                            
-                            <div class="form-group">
-                                <label for="billboardRelays">Relays (one per line):</label>
-                                <textarea id="billboardRelays" rows="3" placeholder="wss://relay.example.com"></textarea>
-                            </div>
-                            
-                            <div class="form-group">
-                                <label for="billboardExpiry">Expiry (hours):</label>
-                                <input type="number" id="billboardExpiry" value="24" min="1" max="720">
+                            <div class="modal-columns">
+                                <div class="modal-column">
+                                    <div class="form-group">
+                                        <label for="billboardDescription">Description:</label>
+                                        <input type="text" id="billboardDescription" placeholder="HTTP-over-Nostr server">
+                                    </div>
+                                    
+                                    <div class="form-group">
+                                        <label for="billboardRelays">Relays:</label>
+                                        <textarea id="billboardRelays" rows="2" placeholder="wss://relay.example.com"></textarea>
+                                    </div>
+                                </div>
+                                
+                                <div class="modal-column">
+                                    <div class="form-group">
+                                        <label>Server Key:</label>
+                                        <div class="pubkey-options">
+                                            <div class="radio-option">
+                                                <input type="radio" id="generatePubkey" name="pubkeyOption" checked>
+                                                <label for="generatePubkey">Generate new key</label>
+                                            </div>
+                                            <div class="radio-option">
+                                                <input type="radio" id="providePubkey" name="pubkeyOption">
+                                                <label for="providePubkey">Use existing key</label>
+                                            </div>
+                                        </div>
+                                        
+                                        <div id="pubkeyInputContainer" class="hidden">
+                                            <input type="text" id="serverPubkey" placeholder="npub or hex pubkey">
+                                        </div>
+                                    </div>
+                                    
+                                    <div class="form-group">
+                                        <label for="billboardExpiry">Expiry:</label>
+                                        <div class="expiry-input-container">
+                                            <input type="number" id="billboardExpiry" value="24" min="1" max="720">
+                                            <span class="expiry-unit">hours</span>
+                                        </div>
+                                    </div>
+                                </div>
                             </div>
                             
                             <div class="form-actions">
                                 <button type="button" id="cancelBillboard" class="secondary-button">Cancel</button>
-                                <button type="submit" id="saveBillboard" class="primary-button">Save</button>
+                                <button type="submit" id="saveBillboard" class="primary-button">Sign & Publish</button>
                             </div>
                         </form>
                     </div>
@@ -91,6 +118,6 @@
         </div>
     </div>
     
-    <!-- Script will be provided by bundle.js -->
+    <!-- Scripts are now loaded as chunks by webpack -->
 </body>
 </html>
\ No newline at end of file
diff --git a/client/eslint.config.js b/client/eslint.config.js
index 6863017..66f2b04 100644
--- a/client/eslint.config.js
+++ b/client/eslint.config.js
@@ -71,7 +71,7 @@ module.exports = [
       ],
 
       // General ESLint rules
-      'no-console': 'warn',
+      'no-console': 'off', // Allowing console statements
       'eqeqeq': ['error', 'always'],
       'no-var': 'error',
       'prefer-const': 'error',
diff --git a/client/help.html b/client/help.html
deleted file mode 100644
index d68198d..0000000
--- a/client/help.html
+++ /dev/null
@@ -1,182 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-<head>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <!-- Implement strict Content Security Policy -->
-    <title>HTTP Messages - Documentation</title>
-    <!-- Load our CSS file -->
-    <link rel="stylesheet" href="./styles.css">
-    <!-- Include the Nostr extensions - these will be accessed via window.nostr -->
-    <script defer src="./bundle.js"></script>
-</head>
-<body>
-    <!-- Top Navigation Bar -->
-    <div class="top-nav">
-        <div class="nav-left">
-            <a href="./index.html" class="nav-link">CLIENT</a>
-            <a href="./receive.html" class="nav-link">SERVER</a>
-            <a href="./billboard.html" class="nav-link">BILLBOARD</a>
-        </div>
-        <div class="nav-right">
-            <a href="./help.html" class="nav-link nav-icon active" title="Documentation">❓</a>
-            <a href="./profile.html" class="nav-link nav-icon" title="Profile">👤</a>
-            <button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Dark Mode">
-                <span id="themeIcon">🌙</span>
-            </button>
-        </div>
-    </div>
-
-    <h1>HTTP Messages</h1>
-    
-    <!-- Documentation Content -->
-    <div class="content">
-        <div class="info-box">
-            <p>A specification for sending/receiving HTTP messages (request/response) via a remote server. Header / Body etc are encrypted, either in the <code>content</code> (small messages) or via a blossom server (larger requests).</p>
-        </div>
-        
-        <div class="diagram-container">
-            <img src="./http.png" alt="HTTP Messages Architecture Diagram">
-            <p class="diagram-caption">HTTP Messages Architecture Overview</p>
-        </div>
-
-        <section class="section">
-            <h2>Overview</h2>
-            <p>HTTP Messages enables a local client to make and receive HTTP requests (PUT, POST, GET, PATCH etc) from a remote computer. This approach provides enhanced privacy, anonymity, and resistance to censorship.</p>
-            
-            <p>The system requires:</p>
-            <ul>
-                <li>A trusted machine to process the messages (can be a home PC or Raspberry Pi)</li>
-                <li>A relay (can be untrusted)</li>
-                <li>A blossom server (can be untrusted)</li>
-            </ul>
-        </section>
-
-        <section class="section">
-            <h2>How It Works</h2>
-            <p>HTTP Messages operates by converting standard HTTP requests into encrypted Nostr events (kind 21120) that can be safely transmitted through untrusted relays. The process ensures privacy and security while enabling communication with the regular internet.</p>
-            
-            <div class="features-grid">
-                <div class="feature-card">
-                    <h3>Privacy Protection</h3>
-                    <p>All HTTP headers and body content are encrypted, protecting sensitive information from intermediaries.</p>
-                </div>
-                <div class="feature-card">
-                    <h3>Censorship Resistance</h3>
-                    <p>By routing through Nostr relays, the system can bypass traditional censorship mechanisms that block direct connections.</p>
-                </div>
-                <div class="feature-card">
-                    <h3>Server Anonymity</h3>
-                    <p>No domain needed or port forwarding required, keeping your server completely private.</p>
-                </div>
-                <div class="feature-card">
-                    <h3>Flexible Architecture</h3>
-                    <p>Works with small payloads (embedded in content) or large requests (via blossom server).</p>
-                </div>
-            </div>
-        </section>
-
-        <section class="section">
-            <h2>Architecture</h2>
-            <p>The HTTP Messages architecture consists of several components working together:</p>
-            
-            <h3>Components</h3>
-            <ul>
-                <li><strong>Nostr Client</strong>: Converts HTTP requests into kind 21120 events and handles responses</li>
-                <li><strong>Nostr Relay</strong>: Transmits events between clients and servers (untrusted)</li>
-                <li><strong>Blossom Storage</strong>: Stores larger payloads that don't fit in event content (untrusted)</li>
-                <li><strong>Trusted Device</strong>: Processes encrypted requests, makes actual HTTP calls, and returns responses</li>
-            </ul>
-
-            <h3>Sequence Diagram</h3>
-            <ol>
-                <li>Client converts HTTP request into kind 21120 event</li>
-                <li>Client encrypts & pushes payload to Blossom (if large)</li>
-                <li>Client publishes event to Nostr relay</li>
-                <li>Trusted device fetches the event</li>
-                <li>Trusted device decrypts event</li>
-                <li>Trusted device fetches payload from Blossom (if large)</li>
-                <li>Trusted device makes the actual HTTP request</li>
-                <li>Trusted device gets HTTP response</li>
-                <li>Trusted device encrypts & pushes response payload to Blossom (if large)</li>
-                <li>Trusted device creates kind 21121 response event</li>
-                <li>Trusted device publishes response event to relay</li>
-                <li>Client fetches response event</li>
-                <li>Client decrypts event</li>
-                <li>Client fetches payload from Blossom (if large)</li>
-                <li>Client converts kind 21121 into HTTP response</li>
-                <li>Client deletes request blob (if exists)</li>
-                <li>Client deletes request event</li>
-            </ol>
-            <p>The remote server should periodically scan for expired RESPONSE events (and associated blossom blobs) and delete them.</p>
-        </section>
-
-        <section class="section">
-            <h2>Event Structure</h2>
-            <p>HTTP Messages uses several Nostr event kinds with specific structures:</p>
-            
-            <h3>Server Advertisement Event (Kind 31120)</h3>
-            <p>Used to facilitate discovery of HTTP-over-Nostr servers:</p>
-            <pre>{
-  "kind": 31120,
-  "pubkey": "&lt;pubkey of server operator&gt;",
-  "content": "HTTP-over-Nostr server",  // Optional markdown description of the http server(s)
-  "tags": [
-    ["d", "&lt;hex pubkey of server&gt;"],  // Server pubkey that will be listening for requests
-    ["relay", "wss://relay.one"],  // Relay where server is listening (can have multiple)
-    ["relay", "wss://relay.two"],
-    ["expiry", "&lt;unix timestamp&gt;"],  // How long this server will be online
-  ]
-}</pre>
-            <p>Clients looking to use HTTP over Nostr can query for these kind 31120 events to discover available servers and may communicate with the server operator to get permission to use them.</p>
-
-            <h3>HTTP Request (Kind 21120)</h3>
-            <pre>{
-  "kind": 21120,
-  "pubkey": "&lt;pubkey&gt;",
-  "content": "$encryptedPayload",
-  "tags": [
-    ["p", "&lt;pubkey of remote server&gt;"], // P tag entry, this is a REQUEST
-    ["key","nip44Encrypt($decryptkey)"],
-    ["r", "https://relay.one"],
-    ["expiration",&lt;unix timestamp&gt;]
-  ]
-}</pre>
-            <p>NIP-44 is NOT used for the content encryption as the payload may be large, affecting bunker signing stability.</p>
-
-            <h3>HTTP Response (Kind 21121)</h3>
-            <pre>{
-  "kind": 21121,
-  "pubkey": "&lt;pubkey&gt;",
-  "content": "encrypt({'url':'blossom.one','hash':'xx'},$decryptkey)",
-  "tags": [
-    ["key","nip44Encrypt($decryptkey)"],
-    ["E", "&lt;request event id&gt;"], // E tag entry, this is a RESPONSE
-    ["expiration",&lt;unix timestamp&gt;]
-  ]
-}</pre>
-            <p>A different kind is used for responses to help with filtering. There is no "p" tag as the "E" tag already identifies the request.</p>
-        </section>
-
-        <section class="section">
-            <h2>Considerations & Use Cases</h2>
-            <p>This approach only makes sense in cases where privacy and anonymity are important, or if censorship is a concern.</p>
-
-            <h3>Drawbacks</h3>
-            <ul>
-                <li><strong>Complexity</strong>: Many more moving parts than a direct request</li>
-                <li><strong>Speed</strong>: Each request/response requires multiple steps (encryption, signing, transmission, decryption, etc.)</li>
-            </ul>
-
-            <h3>Why Use It?</h3>
-            <ul>
-                <li>Enables a plethora of open source apps to be made available from inside private networks (localhost) but over Nostr</li>
-                <li>Maximum server privacy (no domain needed, or port forwarding)</li>
-                <li>Make regular API calls over Nostr</li>
-                <li>Enhanced privacy and anonymity</li>
-                <li>Resistance to censorship</li>
-            </ul>
-        </section>
-    </div>
-</body>
-</html>
\ No newline at end of file
diff --git a/client/index.html b/client/index.html
index 22d224c..3d41a3f 100644
--- a/client/index.html
+++ b/client/index.html
@@ -4,105 +4,169 @@
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <!-- Implement strict Content Security Policy -->
-    <title>HTTP Messages - CLIENT</title>
+    <title>HTTP Messages - Documentation</title>
     <!-- Load our CSS file -->
     <link rel="stylesheet" href="./styles.css">
-    <!-- Include the Nostr extensions - these will be accessed via window.nostr -->
-    <script defer src="./bundle.js"></script>
+    <!-- Include the webpack bundled JavaScript files -->
+    <script defer src="./main.bundle.js"></script>
+    <!-- Additional chunks will be loaded automatically -->
 </head>
 <body>
-    <!-- Top Navigation Bar -->
-    <div class="top-nav">
-        <div class="nav-left">
-            <a href="./index.html" class="nav-link active">CLIENT</a>
-            <a href="./receive.html" class="nav-link">SERVER</a>
-            <a href="./billboard.html" class="nav-link">BILLBOARD</a>
-        </div>
-        <div class="nav-right">
-            <a href="./help.html" class="nav-link nav-icon" title="Documentation">❓</a>
-            <a href="./profile.html" class="nav-link nav-icon" title="Profile">👤</a>
-            <button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Dark Mode">
-                <span id="themeIcon">🌙</span>
-            </button>
-        </div>
+    <!-- Navigation bar container - content will be injected by navbar.ts -->
+    <div id="navbarContainer" class="top-nav">
+        <!-- Navbar content will be injected here -->
     </div>
+
+    <h1>HTTP Messages</h1>
     
-    <!-- Main Content (HTTP Request Converter) -->
+    <!-- Documentation Content -->
     <div class="content">
         <div class="info-box">
-            <p>This tool converts HTTP requests into a single Nostr event of kind 21120. The HTTP request is encrypted using NIP-44 in the content field with appropriate tags following the specification.</p>
+            <p>A specification for sending/receiving HTTP messages (request/response) via a remote server. Header / Body etc are encrypted, either in the <code>content</code> (small messages) or via a blossom server (larger requests).</p>
         </div>
         
-        <h2>Server Information:</h2>
-        <div style="margin-bottom: 15px;">
-            <div style="margin-bottom: 10px;">
-                <label for="relay">Relay to search for servers:</label><br>
-                <div class="server-input-container">
-                    <input type="text" id="relay" value="wss://relay.degmods.com" class="server-input" style="border-radius: 4px 0 0 4px;">
-                    <button id="searchRelayBtn" class="server-search-button">Search Relay</button>
-                    <button id="refreshRelayBtn" class="server-refresh-button" title="Clear cache and fetch fresh data">🔄</button>
-                </div>
-            </div>
-            
-            <div style="margin-bottom: 10px;">
-                <label for="serverSelection">Choose a server:</label><br>
-                <div class="server-input-container">
-                    <input type="text" id="serverPubkey" placeholder="Selected server pubkey (d-tag) or enter manually" class="server-input">
-                    <button id="selectServerBtn" class="server-select-button">Select Server</button>
-                </div>
-                <div id="serverSelectionContainer" class="server-selection-container" style="display: none;">
-                    <div class="selection-header">
-                        <input type="text" id="serverSearchInput" placeholder="Search by operator name/pubkey" class="server-search-input">
-                        <button id="closeSelectionBtn" class="close-selection-btn">&times;</button>
-                    </div>
-                    <div id="serverList" class="server-list">
-                        <!-- Server list will be populated here -->
-                        <div class="server-list-loading">Loading servers...</div>
-                    </div>
-                </div>
-            </div>
-            
-            <div>
-                <label for="relay">Response Relay (optional):</label><br>
-                <input type="text" id="relay" value="wss://relay.degmods.com" style="width: 100%; padding: 8px;">
-            </div>
+        <div class="diagram-container">
+            <img src="./http.png" alt="HTTP Messages Architecture Diagram">
+            <p class="diagram-caption">HTTP Messages Architecture Overview</p>
         </div>
-        
-        <h2>Enter HTTP Request:</h2>
-        <textarea id="httpRequest" placeholder="GET /index.html HTTP/1.1
-Host: example.com
-User-Agent: Browser/1.0
 
-"></textarea>
-        
-        <button id="convertButton">Convert to Event</button>
-        
-        <div id="output" hidden>
-            <h2>Converted Event:</h2>
-            <div class="event-output-container">
-                <pre id="eventOutput"></pre>
-                <button id="copyEventButton" class="copy-button" title="Copy to clipboard">
-                    <span>Copy</span>
-                </button>
-            </div>
+        <section class="section">
+            <h2>Overview</h2>
+            <p>HTTP Messages enables a local client to make and receive HTTP requests (PUT, POST, GET, PATCH etc) from a remote computer. This approach provides enhanced privacy, anonymity, and resistance to censorship.</p>
             
-            <div class="publish-container">
-                <h2>Publish to Relay:</h2>
-                <div class="publish-input-container">
-                    <input type="text" id="publishRelay" value="wss://relay.degmods.com" placeholder="wss://relay.example.com" class="publish-input">
-                    <button id="publishButton" class="publish-button">Publish Event</button>
+            <p>The system requires:</p>
+            <ul>
+                <li>A trusted machine to process the messages (can be a home PC or Raspberry Pi)</li>
+                <li>A relay (can be untrusted)</li>
+                <li>A blossom server (can be untrusted)</li>
+            </ul>
+        </section>
+
+        <section class="section">
+            <h2>How It Works</h2>
+            <p>HTTP Messages operates by converting standard HTTP requests into encrypted Nostr events (kind 21120) that can be safely transmitted through untrusted relays. The process ensures privacy and security while enabling communication with the regular internet.</p>
+            
+            <div class="features-grid">
+                <div class="feature-card">
+                    <h3>Privacy Protection</h3>
+                    <p>All HTTP headers and body content are encrypted, protecting sensitive information from intermediaries.</p>
                 </div>
-                <div id="publishResult" class="publish-result" style="display: none;">
-                    <!-- Publish results will be shown here -->
+                <div class="feature-card">
+                    <h3>Censorship Resistance</h3>
+                    <p>By routing through Nostr relays, the system can bypass traditional censorship mechanisms that block direct connections.</p>
+                </div>
+                <div class="feature-card">
+                    <h3>Server Anonymity</h3>
+                    <p>No domain needed or port forwarding required, keeping your server completely private.</p>
+                </div>
+                <div class="feature-card">
+                    <h3>Flexible Architecture</h3>
+                    <p>Works with small payloads (embedded in content) or large requests (via blossom server).</p>
                 </div>
             </div>
+        </section>
+
+        <section class="section">
+            <h2>Architecture</h2>
+            <p>The HTTP Messages architecture consists of several components working together:</p>
             
-            <div class="qr-container">
-                <h2>QR Code:</h2>
-                <div id="qrCode"></div>
-                <p><small>Scan this QR code to share the Nostr event</small></p>
-            </div>
-        </div>
+            <h3>Components</h3>
+            <ul>
+                <li><strong>Nostr Client</strong>: Converts HTTP requests into kind 21120 events and handles responses</li>
+                <li><strong>Nostr Relay</strong>: Transmits events between clients and servers (untrusted)</li>
+                <li><strong>Blossom Storage</strong>: Stores larger payloads that don't fit in event content (untrusted)</li>
+                <li><strong>Trusted Device</strong>: Processes encrypted requests, makes actual HTTP calls, and returns responses</li>
+            </ul>
+
+            <h3>Sequence Diagram</h3>
+            <ol>
+                <li>Client converts HTTP request into kind 21120 event</li>
+                <li>Client encrypts & pushes payload to Blossom (if large)</li>
+                <li>Client publishes event to Nostr relay</li>
+                <li>Trusted device fetches the event</li>
+                <li>Trusted device decrypts event</li>
+                <li>Trusted device fetches payload from Blossom (if large)</li>
+                <li>Trusted device makes the actual HTTP request</li>
+                <li>Trusted device gets HTTP response</li>
+                <li>Trusted device encrypts & pushes response payload to Blossom (if large)</li>
+                <li>Trusted device creates kind 21121 response event</li>
+                <li>Trusted device publishes response event to relay</li>
+                <li>Client fetches response event</li>
+                <li>Client decrypts event</li>
+                <li>Client fetches payload from Blossom (if large)</li>
+                <li>Client converts kind 21121 into HTTP response</li>
+                <li>Client deletes request blob (if exists)</li>
+                <li>Client deletes request event</li>
+            </ol>
+            <p>The remote server should periodically scan for expired RESPONSE events (and associated blossom blobs) and delete them.</p>
+        </section>
+
+        <section class="section">
+            <h2>Event Structure</h2>
+            <p>HTTP Messages uses several Nostr event kinds with specific structures:</p>
+            
+            <h3>Server Advertisement Event (Kind 31120)</h3>
+            <p>Used to facilitate discovery of HTTP-over-Nostr servers:</p>
+            <pre>{
+  "kind": 31120,
+  "pubkey": "&lt;pubkey of server operator&gt;",
+  "content": "HTTP-over-Nostr server",  // Optional markdown description of the http server(s)
+  "tags": [
+    ["d", "&lt;hex pubkey of server&gt;"],  // Server pubkey that will be listening for requests
+    ["relay", "wss://relay.one"],  // Relay where server is listening (can have multiple)
+    ["relay", "wss://relay.two"],
+    ["expiry", "&lt;unix timestamp&gt;"],  // How long this server will be online
+  ]
+}</pre>
+            <p>Clients looking to use HTTP over Nostr can query for these kind 31120 events to discover available servers and may communicate with the server operator to get permission to use them.</p>
+
+            <h3>HTTP Request (Kind 21120)</h3>
+            <pre>{
+  "kind": 21120,
+  "pubkey": "&lt;pubkey&gt;",
+  "content": "$encryptedPayload",
+  "tags": [
+    ["p", "&lt;pubkey of remote server&gt;"], // P tag entry, this is a REQUEST
+    ["key","nip44Encrypt($decryptkey)"],
+    ["r", "https://relay.one"],
+    ["expiration",&lt;unix timestamp&gt;]
+  ]
+}</pre>
+            <p>NIP-44 is NOT used for the content encryption as the payload may be large, affecting bunker signing stability.</p>
+
+            <h3>HTTP Response (Kind 21121)</h3>
+            <pre>{
+  "kind": 21121,
+  "pubkey": "&lt;pubkey&gt;",
+  "content": "encrypt({'url':'blossom.one','hash':'xx'},$decryptkey)",
+  "tags": [
+    ["key","nip44Encrypt($decryptkey)"],
+    ["E", "&lt;request event id&gt;"], // E tag entry, this is a RESPONSE
+    ["expiration",&lt;unix timestamp&gt;]
+  ]
+}</pre>
+            <p>A different kind is used for responses to help with filtering. There is no "p" tag as the "E" tag already identifies the request.</p>
+        </section>
+
+        <section class="section">
+            <h2>Considerations & Use Cases</h2>
+            <p>This approach only makes sense in cases where privacy and anonymity are important, or if censorship is a concern.</p>
+
+            <h3>Drawbacks</h3>
+            <ul>
+                <li><strong>Complexity</strong>: Many more moving parts than a direct request</li>
+                <li><strong>Speed</strong>: Each request/response requires multiple steps (encryption, signing, transmission, decryption, etc.)</li>
+            </ul>
+
+            <h3>Why Use It?</h3>
+            <ul>
+                <li>Enables a plethora of open source apps to be made available from inside private networks (localhost) but over Nostr</li>
+                <li>Maximum server privacy (no domain needed, or port forwarding)</li>
+                <li>Make regular API calls over Nostr</li>
+                <li>Enhanced privacy and anonymity</li>
+                <li>Resistance to censorship</li>
+            </ul>
+        </section>
     </div>
 </body>
 </html>
\ No newline at end of file
diff --git a/client/package.json b/client/package.json
index 5e9f86f..284edba 100644
--- a/client/package.json
+++ b/client/package.json
@@ -5,9 +5,8 @@
   "main": "dist/index.html",
   "scripts": {
     "build": "webpack --mode production",
-    "copy-assets": "mkdir -p dist && cp index.html help.html dist/ && cp http.png dist/ 2>/dev/null || true",
-    "dev": "webpack serve --open",
-    "start": "npm run build && npm run copy-assets && npx serve dist",
+    "dev": "webpack serve",
+    "start": "npm run build && npx serve dist",
     "clean": "rm -rf dist",
     "lint": "eslint . --ext .ts",
     "lint:fix": "eslint . --ext .ts --fix",
diff --git a/client/profile.html b/client/profile.html
index 4198593..75e40ad 100644
--- a/client/profile.html
+++ b/client/profile.html
@@ -5,23 +5,13 @@
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>HTTP Messages - Profile</title>
     <link rel="stylesheet" href="./styles.css">
-    <script defer src="./bundle.js"></script>
+    <script defer src="./main.bundle.js"></script>
+    <!-- Additional chunks will be loaded automatically -->
 </head>
 <body>
-    <!-- Top Navigation Bar -->
-    <div class="top-nav">
-        <div class="nav-left">
-            <a href="./index.html" class="nav-link">CLIENT</a>
-            <a href="./receive.html" class="nav-link">SERVER</a>
-            <a href="./billboard.html" class="nav-link">BILLBOARD</a>
-        </div>
-        <div class="nav-right">
-            <a href="./help.html" class="nav-link nav-icon" title="Documentation">❓</a>
-            <a href="./profile.html" class="nav-link nav-icon active" title="Profile">👤</a>
-            <button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Dark Mode">
-                <span id="themeIcon">🌙</span>
-            </button>
-        </div>
+    <!-- Navigation bar container - content will be injected by navbar.ts -->
+    <div id="navbarContainer" class="top-nav">
+        <!-- Navbar content will be injected here -->
     </div>
     
     <!-- Main Content -->
@@ -97,6 +87,6 @@
         </div>
     </div>
     
-    <!-- Profile script will be loaded from bundle.js -->
+    <!-- Scripts are now loaded as chunks by webpack -->
 </body>
 </html>
\ No newline at end of file
diff --git a/client/src/billboard.ts b/client/src/billboard.ts
index 0c8464f..77b2ebc 100644
--- a/client/src/billboard.ts
+++ b/client/src/billboard.ts
@@ -5,6 +5,7 @@ import * as nostrTools from 'nostr-tools';
 
 import { defaultServerConfig } from './config';
 import { NostrService } from './services/NostrService';
+import { toggleTheme } from './theme-utils';
 
 // Module-level variables
 let nostrService: NostrService;
@@ -45,6 +46,12 @@ function setupUIElements(): void {
     connectButton.addEventListener('click', handleConnectRelay);
   }
   
+  // Set up filter checkbox event listener
+  const showAllServerEventsCheckbox = document.getElementById('showAllServerEvents') as HTMLInputElement;
+  if (showAllServerEventsCheckbox) {
+    showAllServerEventsCheckbox.addEventListener('change', handleFilterChange);
+  }
+  
   // Create billboard button
   const createBillboardBtn = document.getElementById('createBillboardBtn');
   if (createBillboardBtn) {
@@ -69,6 +76,21 @@ function setupUIElements(): void {
     billboardForm.addEventListener('submit', handleSaveBillboard);
   }
   
+  // Server pubkey radio buttons
+  const generatePubkeyRadio = document.getElementById('generatePubkey') as HTMLInputElement;
+  const providePubkeyRadio = document.getElementById('providePubkey') as HTMLInputElement;
+  const pubkeyInputContainer = document.getElementById('pubkeyInputContainer');
+  
+  if (generatePubkeyRadio && providePubkeyRadio && pubkeyInputContainer) {
+    generatePubkeyRadio.addEventListener('change', () => {
+      pubkeyInputContainer.classList.add('hidden');
+    });
+    
+    providePubkeyRadio.addEventListener('change', () => {
+      pubkeyInputContainer.classList.remove('hidden');
+    });
+  }
+  
   // Set dark mode based on localStorage
   const savedTheme = window.localStorage.getItem('theme');
   if (savedTheme === 'dark') {
@@ -85,11 +107,7 @@ function setupUIElements(): void {
     }
   }
   
-  // Set up theme toggle
-  const themeToggleBtn = document.getElementById('themeToggleBtn');
-  if (themeToggleBtn) {
-    themeToggleBtn.addEventListener('click', toggleTheme);
-  }
+  // Theme toggle is handled by navbar.ts, don't add duplicate event listeners here
 }
 
 /**
@@ -123,11 +141,15 @@ async function handleConnectRelay(): Promise<void> {
   const success = await nostrService.connectToRelay(relayUrl);
   if (success) {
     try {
-      // Subscribe to kind 31120 events
-      await subscribeToKind31120Events();
+      // Get the checkbox state
+      const showAllServerEventsCheckbox = document.getElementById('showAllServerEvents') as HTMLInputElement;
+      const showAllEvents = showAllServerEventsCheckbox ? showAllServerEventsCheckbox.checked : false;
+      
+      // Subscribe to kind 31120 events with filter state
+      await subscribeToKind31120Events(showAllEvents);
     } catch (error) {
       updateRelayStatus(
-        `Subscription error: ${error instanceof Error ? error.message : String(error)}`, 
+        `Subscription error: ${error instanceof Error ? error.message : String(error)}`,
         'error'
       );
     }
@@ -135,15 +157,87 @@ async function handleConnectRelay(): Promise<void> {
 }
 
 /**
- * Subscribe to kind 31120 events
+ * Handle filter checkbox changes
  */
-async function subscribeToKind31120Events(): Promise<void> {
+async function handleFilterChange(): Promise<void> {
+  // Only resubscribe if we're already connected to a relay
+  if (nostrService.isConnected()) {
+    const showAllServerEventsCheckbox = document.getElementById('showAllServerEvents') as HTMLInputElement;
+    if (!showAllServerEventsCheckbox) {
+      return;
+    }
+    
+    console.log(`Checkbox changed to: ${showAllServerEventsCheckbox.checked ? 'show all events' : 'only show my events'}`);
+    
+    try {
+      // Update status to indicate resubscription is in progress
+      updateRelayStatus('Updating subscription...', 'connecting');
+      
+      // Resubscribe with the new filter setting
+      await subscribeToKind31120Events(showAllServerEventsCheckbox.checked);
+      
+      console.log(`Subscription updated with new filter settings`);
+      updateRelayStatus('Subscription updated ✓', 'connected');
+    } catch (error) {
+      console.error("Failed to update subscription:", error);
+      updateRelayStatus(
+        `Subscription update failed: ${error instanceof Error ? error.message : String(error)}`,
+        'error'
+      );
+    }
+  } else {
+    console.log("Checkbox changed but no active relay connection");
+  }
+}
+
+/**
+ * Subscribe to kind 31120 events
+ * @param showAllEvents Whether to show all events or only those created by the current user
+ */
+async function subscribeToKind31120Events(showAllEvents: boolean = false): Promise<void> {
+  // Clear the billboard content area
+  const billboardContent = document.getElementById('billboardContent');
+  if (billboardContent) {
+    billboardContent.innerHTML = '<div class="empty-state">Loading server registrations...</div>';
+  }
+
   // Create filter for kind 31120 events
-  const filter = {
+  const filter: any = {
     kinds: [31120], // HTTP Messages server advertisements
   };
   
-  updateRelayStatus('Subscribing to server advertisements...', 'connecting');
+  // If not showing all events, filter by the current user's pubkey
+  if (!showAllEvents) {
+    // Get the logged-in user's pubkey
+    const userPubkey = nostrService.getLoggedInPubkey();
+    
+    if (userPubkey) {
+      // Convert npub to hex if needed
+      let userPubkeyHex = userPubkey;
+      if (userPubkey.startsWith('npub')) {
+        try {
+          const decoded = nostrTools.nip19.decode(userPubkey);
+          if (decoded.type === 'npub') {
+            userPubkeyHex = decoded.data as string;
+          }
+        } catch (error) {
+          console.error("Error decoding npub:", error);
+          // Use the original value if conversion fails
+        }
+      }
+      
+      console.log(`Filtering for events by author: ${userPubkeyHex.substring(0, 8)}...`);
+      filter.authors = [userPubkeyHex];
+    } else {
+      console.warn("No user pubkey available, showing all events despite filter setting");
+    }
+  }
+  // Update status message based on filter
+  const statusMessage = showAllEvents
+    ? 'Subscribing to all server advertisements...'
+    : 'Subscribing to your server advertisements...';
+  
+  updateRelayStatus(statusMessage, 'connecting');
   
   try {
     // Subscribe to events
@@ -155,7 +249,12 @@ async function subscribeToKind31120Events(): Promise<void> {
       }
     });
     
-    updateRelayStatus('Connected and listening for server events ✓', 'connected');
+    // Update status based on filter
+    const successMessage = showAllEvents
+      ? 'Connected and listening for all server events ✓'
+      : 'Connected and listening for your server events ✓';
+    
+    updateRelayStatus(successMessage, 'connected');
   } catch (error) {
     throw new Error(`Failed to subscribe: ${error instanceof Error ? error.message : String(error)}`);
   }
@@ -163,8 +262,6 @@ async function subscribeToKind31120Events(): Promise<void> {
 
 /**
  * Process a server advertisement event (kind 31120)
- */
-/**
  * Open the modal for creating a new billboard
  */
 function handleCreateBillboard(): void {
@@ -263,45 +360,81 @@ function handleEditBillboard(event: nostrTools.Event): void {
 async function handleSaveBillboard(e: Event): Promise<void> {
   e.preventDefault();
   
-  // Get form values
-  const editEventId = (document.getElementById('editEventId') as HTMLInputElement)?.value;
-  const description = (document.getElementById('billboardDescription') as HTMLInputElement)?.value || 'HTTP-over-Nostr server';
-  const relaysText = (document.getElementById('billboardRelays') as HTMLTextAreaElement)?.value || '';
-  const expiryHours = parseInt((document.getElementById('billboardExpiry') as HTMLInputElement)?.value || '24');
-  
-  // Parse relay URLs
-  const relays = relaysText
-    .split('\n')
-    .map(line => line.trim())
-    .filter(line => line && line.startsWith('wss://'));
-  
-  // Validate input
-  if (relays.length === 0) {
-    alert('Please provide at least one valid relay URL (starting with wss://)');
-    return;
-  }
-  
-  // Get the current relay URL for publishing
-  const relayUrlInput = document.getElementById('billboardRelayUrl') as HTMLInputElement;
-  const relayUrl = relayUrlInput?.value.trim() || relays[0];
-  
   try {
+    // Check if user is logged in
+    const loggedInPubkey = nostrService.getLoggedInPubkey();
+    if (!loggedInPubkey) {
+      alert('You need to be logged in to publish a billboard. Please visit the Profile page to log in.');
+      return;
+    }
+    
+    // Get form values
+    const editEventId = (document.getElementById('editEventId') as HTMLInputElement)?.value;
+    const description = (document.getElementById('billboardDescription') as HTMLInputElement)?.value || 'HTTP-over-Nostr server';
+    const relaysText = (document.getElementById('billboardRelays') as HTMLTextAreaElement)?.value || '';
+    const expiryHours = parseInt((document.getElementById('billboardExpiry') as HTMLInputElement)?.value || '24');
+    
+    // Parse relay URLs
+    const relays = relaysText
+      .split('\n')
+      .map(line => line.trim())
+      .filter(line => line && line.startsWith('wss://'));
+    
+    // Validate input
+    if (relays.length === 0) {
+      alert('Please provide at least one valid relay URL (starting with wss://)');
+      return;
+    }
+    
+    // Get the current relay URL for publishing
+    const relayUrlInput = document.getElementById('billboardRelayUrl') as HTMLInputElement;
+    const relayUrl = relayUrlInput?.value.trim() || relays[0];
+    
+    // Check which pubkey option is selected
+    const generatePubkeyRadio = document.getElementById('generatePubkey') as HTMLInputElement;
+    const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
+    
+    // Determine if we're using a custom pubkey or generating one
+    let customServerPubkey: string | undefined;
+    if (!generatePubkeyRadio?.checked && serverPubkeyInput?.value) {
+      customServerPubkey = serverPubkeyInput.value.trim();
+    }
+    
     // Create or update the billboard event
-    const event = await nostrService.createOrUpdate31120Event(
+    const result = await nostrService.createOrUpdate31120Event(
       relayUrl,
       description,
       relays,
       expiryHours,
-      editEventId || undefined
+      editEventId || undefined,
+      customServerPubkey
     );
     
-    if (event) {
+    if (result) {
       // Add the event to the UI
-      processServerEvent(event as nostrTools.Event);
+      processServerEvent(result.event as nostrTools.Event);
       
-      // Close the modal
-      closeModal();
+      // If we have a server nsec, save it and display it to the user
+      if (result.serverNsec && result.event) {
+        // Save the server nsec using the exact key expected by the receiver.ts
+        localStorage.setItem('serverNsec', result.serverNsec);
+        
+        // Show the nsec in an alert with instructions
+        const message = `
+Server created successfully!
+IMPORTANT: Your server's private key has been generated and saved.
+The server page will automatically use this key.
+For your records, here is the key:
+
+${result.serverNsec}
+This private key is needed to run the server for this billboard.
+`;
+        alert(message);
+      }
     }
+    
+    // Close the modal
+    closeModal();
   } catch (error) {
     console.error('Error saving billboard:', error);
     alert(`Failed to save billboard: ${error instanceof Error ? error.message : String(error)}`);
@@ -413,13 +546,36 @@ function processServerEvent(event: nostrTools.Event): void {
     }
   }
   
+  // Check if the event is owned by the current user
+  const loggedInPubkey = nostrService.getLoggedInPubkey();
+  let userPubkeyHex = loggedInPubkey || '';
+  let isUserOwnedEvent = false;
+  
+  // Convert pubkeys to hex if needed for comparison
+  if (loggedInPubkey && loggedInPubkey.startsWith('npub')) {
+    try {
+      const hexPubkey = nostrTools.nip19.decode(loggedInPubkey).data as string;
+      if (hexPubkey) {
+        userPubkeyHex = hexPubkey;
+      }
+    } catch {
+      // Keep original if conversion fails
+    }
+  }
+  
+  // Check if the user is the creator of this event
+  isUserOwnedEvent = userPubkeyHex === event.pubkey;
+  
   // Create a card for the event
   const eventCard = document.createElement('div');
-  eventCard.className = 'billboard-card';
+  eventCard.className = isUserOwnedEvent ? 'billboard-card user-owned' : 'billboard-card';
   eventCard.id = `event-${event.id}`;
   eventCard.innerHTML = `
     <div class="billboard-card-header">
-      <h3 class="billboard-title">Server Registration</h3>
+      <h3 class="billboard-title">
+        Server Registration
+        ${isUserOwnedEvent ? '<span class="user-owned-indicator">✓ Your event</span>' : ''}
+      </h3>
       <div class="billboard-timestamp">
         <span class="event-created">Created: ${new Date(event.created_at * 1000).toLocaleString()}</span>
         <span class="event-updated" data-time="${new Date(event.created_at * 1000).toISOString()}">Updated: ${new Date(event.created_at * 1000).toLocaleString()}</span>
@@ -447,7 +603,7 @@ function processServerEvent(event: nostrTools.Event): void {
     </div>
     <div class="billboard-card-footer">
       <button class="view-raw-btn" data-id="${event.id}">View Raw JSON</button>
-      <button class="billboard-edit-btn" data-id="${event.id}">Edit</button>
+      <button class="billboard-edit-btn" data-id="${event.id}" ${!isUserOwnedEvent ? 'disabled' : ''}>Edit</button>
     </div>
     <div class="raw-json-content hidden" id="raw-${event.id}">
       <pre>${JSON.stringify(event, null, 2)}</pre>
@@ -482,33 +638,14 @@ function processServerEvent(event: nostrTools.Event): void {
       const target = e.target as HTMLElement;
       const eventId = target.getAttribute('data-id');
       if (eventId) {
-        // Check if the user is the creator of this event
+        // Check if user is logged in
         const loggedInPubkey = nostrService.getLoggedInPubkey();
         if (!loggedInPubkey) {
           alert('You need to be logged in to edit a billboard. Please visit the Profile page to log in.');
           return;
         }
-
-        // Convert pubkeys to hex if needed for comparison
-        let userPubkeyHex = loggedInPubkey;
-        if (loggedInPubkey.startsWith('npub')) {
-          try {
-            const hexPubkey = nostrTools.nip19.decode(loggedInPubkey).data as string;
-            if (hexPubkey) {
-              userPubkeyHex = hexPubkey;
-            }
-          } catch {
-            // Keep original if conversion fails
-          }
-        }
-
-        // Only allow editing if user is the creator
-        if (userPubkeyHex !== event.pubkey) {
-          alert('You can only edit billboards that you created');
-          return;
-        }
-
-        // Open edit modal
+        
+        // Open edit modal (button is already disabled for non-owned events)
         handleEditBillboard(event);
       }
     });
@@ -527,6 +664,12 @@ async function autoConnectToDefaultRelay(): Promise<void> {
       relayUrlInput.value = defaultServerConfig.defaultRelay;
     }
     
+    // Initialize checkbox state (default unchecked - only show user's events)
+    const showAllServerEventsCheckbox = document.getElementById('showAllServerEvents') as HTMLInputElement;
+    if (showAllServerEventsCheckbox) {
+      showAllServerEventsCheckbox.checked = false;
+    }
+    
     // Trigger connect button click
     const connectButton = document.getElementById('billboardConnectBtn');
     if (connectButton) {
@@ -536,36 +679,20 @@ async function autoConnectToDefaultRelay(): Promise<void> {
 }
 
 /**
- * Toggle between light and dark theme
+ * Extract the server pubkey from a 31120 event
+ * @param event The 31120 event to extract pubkey from
+ * @returns Server pubkey or null if not found
  */
-function toggleTheme(): void {
-  const body = document.body;
-  const themeIcon = document.getElementById('themeIcon');
-  const themeText = document.getElementById('themeText');
+function getServerPubkeyFromEvent(event: nostrTools.Event): string | null {
+  if (event.kind !== 31120) return null;
   
-  const isDarkMode = body.getAttribute('data-theme') === 'dark';
-  
-  if (isDarkMode) {
-    // Switch to light theme
-    body.removeAttribute('data-theme');
-    window.localStorage.setItem('theme', 'light');
-    
-    if (themeIcon) {
-      themeIcon.textContent = '🌙';
-    }
-    if (themeText) {
-      themeText.textContent = 'Dark Mode';
-    }
-  } else {
-    // Switch to dark theme
-    body.setAttribute('data-theme', 'dark');
-    window.localStorage.setItem('theme', 'dark');
-    
-    if (themeIcon) {
-      themeIcon.textContent = '☀️';
-    }
-    if (themeText) {
-      themeText.textContent = 'Light Mode';
-    }
+  // Find the d tag which contains the server pubkey
+  const dTag = event.tags.find(tag => tag[0] === 'd');
+  if (dTag && dTag.length > 1) {
+    return dTag[1];
   }
-}
\ No newline at end of file
+  
+  return null;
+}
+
+// We now use the imported toggleTheme function from theme-utils.ts
\ No newline at end of file
diff --git a/client/src/client.ts b/client/src/client.ts
index 48a0dc2..6d38946 100644
--- a/client/src/client.ts
+++ b/client/src/client.ts
@@ -1,19 +1,40 @@
 // client.ts - External TypeScript file for HTTP to Nostr converter
 // This follows strict CSP policies by avoiding inline scripts
 
+// IMPORTANT: Immediately import all critical modules and execute them
+// This ensures they are included in the bundle and executed immediately
+import './navbar';
+import './navbar-init';
+
+// Direct console log that should appear no matter what
+(function() {
+    console.log('%c CLIENT.TS LOADED', 'background: red; color: white; padding: 10px; font-size: 20px;');
+    
+    // Try to add a visible element to confirm code is running
+    if (typeof document !== 'undefined') {
+        window.addEventListener('load', function() {
+            console.log('%c[Client] Window load event - ensuring navbar is visible', 'background: blue; color: white');
+            
+            // Check if navbar is visible, if not try to initialize it
+            const navbarElements = document.querySelectorAll('.nav-left, .nav-right');
+            if (navbarElements.length === 0) {
+                console.log('%c[Client] No navbar elements found on window.load, attempting to initialize', 'background: red; color: white');
+                try {
+                    // Attempt to initialize navbar directly with the imported function
+                    initializeNavbar();
+                } catch (error) {
+                    console.error('[Client] Error initializing navbar on window.load:', error);
+                }
+            }
+            
+            // Removed debug element that was overlapping with the navbar
+        });
+    }
+})();
+
 // Import from Node.js built-ins & external modules
 import * as nostrTools from 'nostr-tools';
 
-// On page load, always fetch the latest pubkey from window.nostr
-if (window.nostr && typeof window.nostr.getPublicKey === 'function') {
-    window.nostr.getPublicKey().then(pubkey => {
-        console.log(`Page load: Retrieved pubkey from window.nostr: ${pubkey.slice(0, 8)}...`);
-        localStorage.setItem('userPublicKey', pubkey);
-    }).catch(err => {
-        console.warn("Page load: Failed to get pubkey from window.nostr:", err);
-    });
-}
-
 // Import type definitions
 import type { NostrEvent } from './converter';
 // Import functions from internal modules
@@ -24,7 +45,20 @@ import { publishToRelay, convertNpubToHex, verifyEvent } from './relay';
 import './profile';
 import './receiver'; // Import receiver module for relay connections and subscriptions
 import './billboard'; // Import billboard module for server registration display
+import { HttpFormatter } from './services/HttpFormatter'; // Import formatter for HTTP content
 import { NostrService } from './services/NostrService';
+import { Nostr31120Service } from './services/Nostr31120Service'; // Import our new dedicated service
+import { getUserPubkey } from './services/NostrUtils';
+import { initializeNavbar } from './navbar';
+
+// Immediately initialize the navbar to ensure it's visible on page load
+try {
+  console.log('%c[CLIENT] Explicitly initializing navbar from client.ts', 'background: purple; color: white; padding: 4px;');
+  initializeNavbar();
+} catch (e) {
+  console.error('[CLIENT] Error initializing navbar:', e);
+}
+
 import {
     sanitizeText,
     setDefaultHttpRequest,
@@ -34,6 +68,51 @@ import {
     standardizeEvent
 } from './utils';
 
+// Import toggleTheme from theme-utils instead of utils
+import { toggleTheme } from './theme-utils';
+
+// On page load, always fetch the latest pubkey from window.nostr
+getUserPubkey().then(pubkey => {
+  if (pubkey) {
+    console.log(`Page load: Retrieved pubkey: ${pubkey.slice(0, 8)}...`);
+    // Display the pubkey in the UI
+    updateClientPubkeyDisplay(pubkey);
+  }
+});
+
+/**
+ * Update the client pubkey display in the UI
+ */
+function updateClientPubkeyDisplay(pubkey: string): void {
+  const clientPubkeyElement = document.getElementById('clientPubkey');
+  if (clientPubkeyElement) {
+    // Determine if it's already in hex or npub format
+    let npubFormat: string;
+    let hexFormat: string;
+    
+    try {
+      if (pubkey.startsWith('npub')) {
+        npubFormat = pubkey;
+        const decoded = nostrTools.nip19.decode(pubkey);
+        hexFormat = decoded.data as string;
+      } else {
+        // It's a hex format
+        hexFormat = pubkey;
+        npubFormat = nostrTools.nip19.npubEncode(pubkey);
+      }
+      
+      // Store both formats for toggling
+      clientPubkeyElement.textContent = npubFormat;
+      clientPubkeyElement.dataset.npub = npubFormat;
+      clientPubkeyElement.dataset.hex = hexFormat;
+    } catch (error) {
+      // If conversion fails, just use the original value
+      clientPubkeyElement.textContent = pubkey;
+      console.error('Error converting client pubkey:', error);
+    }
+  }
+}
+
 // Import nostr-login using CommonJS pattern
 // eslint-disable-next-line no-undef
 const NostrLogin = typeof require !== 'undefined' ? require('nostr-login') : null;
@@ -45,9 +124,15 @@ declare global {
     }
 }
 
-// Add the NostrService instance for handling 31120 events
+// Add the NostrService instance for general Nostr operations
 const nostrService = new NostrService();
 
+// Add the dedicated service for 31120 events (using the underlying services from NostrService)
+const nostr31120Service = new Nostr31120Service(
+  nostrService.getRelayService(),
+  nostrService.getCacheService()
+);
+
 /**
  * Handle showing the server selection modal
  */
@@ -159,22 +244,21 @@ async function handleServerSelection(): Promise<void> {
 
 /**
  * Fetch all kind 31120 events from the specified relay
- * This function leverages the NostrService's built-in caching for 31120 events
+ * This function uses our dedicated 31120 service with built-in caching
  */
 async function fetch31120Events(relayUrl: string): Promise<{[key: string]: NostrEvent}> {
   try {
     // Create an object to store events by server pubkey
     const eventsMap: {[key: string]: NostrEvent} = {};
     
-    // Query for all kind 31120 events using our method that supports caching
-    const events = await nostrService.queryForAll31120Events(relayUrl);
+    // Query for all kind 31120 events using our specialized service
+    const events = await nostr31120Service.queryForAll31120Events(relayUrl);
     
     // Process collected events and organize by server pubkey
     for (const event of events) {
-      // Find the d tag which contains the server pubkey
-      const dTag = event.tags.find((tag: string[]) => tag[0] === 'd');
-      if (dTag && dTag.length > 1) {
-        const serverPubkey = dTag[1];
+      // Get the server pubkey using our service
+      const serverPubkey = nostr31120Service.getServerPubkeyFromEvent(event);
+      if (serverPubkey) {
         // Store the event with the server pubkey as the key
         eventsMap[serverPubkey] = event;
       }
@@ -315,6 +399,7 @@ async function renderServerList(container: HTMLElement, events: {[key: string]:
  */
 const LOCAL_STORAGE_KEYS = {
   SELECTED_SERVER: 'selected_server_pubkey',
+  SELECTED_SERVER_HEX: 'selected_server_pubkey_hex',
   SELECTED_SERVER_NAME: 'selected_server_name'
 };
 
@@ -326,15 +411,29 @@ function selectServer(serverPubkey: string, operatorName: string): void {
     const serverSelectionContainer = document.getElementById('serverSelectionContainer');
     
     if (serverPubkeyInput) {
-        // Make sure we're using consistent format - convert to npub if it's a hex pubkey
+        // Store both original format and the converted format
+        // We'll always store the npub format in the input field for consistency
+        let npubFormat: string;
+        let hexFormat: string;
+        
         try {
-            // If it doesn't start with npub, it's probably a hex format
-            if (!serverPubkey.startsWith('npub')) {
-                const npub = nostrTools.nip19.npubEncode(serverPubkey);
-                serverPubkeyInput.value = npub;
+            // Determine if it's hex or npub, and store both formats
+            if (serverPubkey.startsWith('npub')) {
+                npubFormat = serverPubkey;
+                const decoded = nostrTools.nip19.decode(serverPubkey);
+                hexFormat = decoded.data as string;
             } else {
-                serverPubkeyInput.value = serverPubkey;
+                // It's a hex format
+                hexFormat = serverPubkey;
+                npubFormat = nostrTools.nip19.npubEncode(serverPubkey);
             }
+            
+            // Store the npub format in the input field
+            serverPubkeyInput.value = npubFormat;
+            
+            // Store both formats in data attributes for easy access when switching
+            serverPubkeyInput.dataset.npub = npubFormat;
+            serverPubkeyInput.dataset.hex = hexFormat;
         } catch (error) {
             // If conversion fails, just use the original value
             serverPubkeyInput.value = serverPubkey;
@@ -350,7 +449,14 @@ function selectServer(serverPubkey: string, operatorName: string): void {
     try {
         // Only store it if it's valid
         if (serverPubkey) {
+            // Store the value that's displayed in the input field (typically npub format)
             localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_SERVER, serverPubkeyInput?.value || serverPubkey);
+            
+            // Also store hex format separately for format switching
+            if (serverPubkeyInput?.dataset.hex) {
+                localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_SERVER_HEX, serverPubkeyInput.dataset.hex);
+            }
+            
             localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_SERVER_NAME, operatorName);
         }
     } catch (error) {
@@ -378,24 +484,78 @@ function selectServer(serverPubkey: string, operatorName: string): void {
 function loadSavedServer(): void {
     try {
         const savedPubkey = localStorage.getItem(LOCAL_STORAGE_KEYS.SELECTED_SERVER);
+        const savedPubkeyHex = localStorage.getItem(LOCAL_STORAGE_KEYS.SELECTED_SERVER_HEX);
         const savedOperatorName = localStorage.getItem(LOCAL_STORAGE_KEYS.SELECTED_SERVER_NAME) || 'Unknown';
         
+        const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
+        if (!serverPubkeyInput) {
+            console.error('Server pubkey input element not found');
+            return;
+        }
+        
+        // If we have a saved server pubkey, use that
         if (savedPubkey) {
-            const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
-            if (serverPubkeyInput) {
-                serverPubkeyInput.value = savedPubkey;
+            // Set the visible value to the saved npub format
+            serverPubkeyInput.value = savedPubkey;
+            
+            // Also store both formats as data attributes for toggling
+            if (savedPubkeyHex) {
+                serverPubkeyInput.dataset.hex = savedPubkeyHex;
+                serverPubkeyInput.dataset.npub = savedPubkey;
+            }
+            
+            // Show indicator that we're using a saved selection
+            const selectButton = document.getElementById('selectServerBtn');
+            if (selectButton) {
+                selectButton.textContent = `Using ${savedOperatorName}'s Server`;
+                selectButton.style.backgroundColor = 'var(--button-success)';
                 
-                // Show indicator that we're using a saved selection
-                const selectButton = document.getElementById('selectServerBtn');
-                if (selectButton) {
-                    selectButton.textContent = `Using ${savedOperatorName}'s Server`;
-                    selectButton.style.backgroundColor = 'var(--button-success)';
-                    
-                    // Reset the button after 2 seconds
-                    setTimeout(() => {
-                        selectButton.textContent = 'Select Server';
-                        selectButton.style.backgroundColor = '';
-                    }, 2000);
+                // Reset the button after 2 seconds
+                setTimeout(() => {
+                    selectButton.textContent = 'Select Server';
+                    selectButton.style.backgroundColor = '';
+                }, 2000);
+            }
+        } else {
+            // If no saved server, check for default server pubkey from localStorage
+            // This would have been set in the server page (1120_server.html)
+            const serverNsec = localStorage.getItem('serverNsec');
+            if (serverNsec) {
+                try {
+                    // Extract the server pubkey from the saved nsec
+                    const decoded = nostrTools.nip19.decode(serverNsec);
+                    if (decoded.type === 'nsec') {
+                        const privateKeyBytes = decoded.data as any;
+                        const serverPubkeyHex = nostrTools.getPublicKey(privateKeyBytes);
+                        const serverPubkeyNpub = nostrTools.nip19.npubEncode(serverPubkeyHex);
+                        
+                        console.log(`Using default server pubkey from localStorage: ${serverPubkeyHex.substring(0, 8)}...`);
+                        
+                        // Set values in the input
+                        serverPubkeyInput.value = serverPubkeyNpub;
+                        serverPubkeyInput.dataset.hex = serverPubkeyHex;
+                        serverPubkeyInput.dataset.npub = serverPubkeyNpub;
+                        
+                        // Update localStorage to save this choice
+                        localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_SERVER, serverPubkeyNpub);
+                        localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_SERVER_HEX, serverPubkeyHex);
+                        localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_SERVER_NAME, 'Default');
+                        
+                        // Show indicator that we're using the default server
+                        const selectButton = document.getElementById('selectServerBtn');
+                        if (selectButton) {
+                            selectButton.textContent = 'Using Default Server';
+                            selectButton.style.backgroundColor = 'var(--button-success)';
+                            
+                            // Reset the button after 2 seconds
+                            setTimeout(() => {
+                                selectButton.textContent = 'Select Server';
+                                selectButton.style.backgroundColor = '';
+                            }, 2000);
+                        }
+                    }
+                } catch (error) {
+                    console.error('Error decoding server nsec:', error);
                 }
             }
         }
@@ -406,8 +566,16 @@ function loadSavedServer(): void {
 
 /**
  * Initialize nostr-login
+ * Prevents multiple initializations by checking if it's already been initialized
  */
 function initNostrLogin(): void {
+    // Check if we've already initialized NostrLogin in this session
+    const nostrLoginInitialized = window.sessionStorage.getItem('nostrLoginInitialized');
+    if (nostrLoginInitialized === 'true') {
+        console.log("NostrLogin already initialized in this session, skipping");
+        return;
+    }
+    
     try {
         // Initialize NostrLogin without requiring UI elements
         if (NostrLogin && NostrLogin.init) {
@@ -424,18 +592,131 @@ function initNostrLogin(): void {
                     console.log(`Connected to Nostr with pubkey: ${pubkey.slice(0, 8)}...`);
                     // Store pubkey in localStorage for other parts of the app
                     localStorage.setItem('userPublicKey', pubkey);
+                    // Update UI to show pubkey
+                    updateClientPubkeyDisplay(pubkey);
                 },
                 onDisconnect: (): void => {
                     console.log('Disconnected from Nostr');
                     localStorage.removeItem('userPublicKey');
+                },
+                // Add error handler to catch "Already started" errors
+                onError: (error: Error): void => {
+                    console.error("NostrLogin error:", error);
+                    
+                    if (error.message === 'Already started') {
+                        console.log("Detected 'Already started' error. Resetting state...");
+                        // Clear auth state manually
+                        window.sessionStorage.removeItem('nostrLoginInitialized');
+                        ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
+                            window.sessionStorage.removeItem(key);
+                        });
+                    }
                 }
             });
             
+            // Mark as initialized to prevent duplicate initialization
+            window.sessionStorage.setItem('nostrLoginInitialized', 'true');
+            
             // Check if we can get an existing pubkey (already connected)
             if (window.nostr) {
                 window.nostr.getPublicKey().then(pubkey => {
                     console.log(`Already connected with pubkey: ${pubkey.slice(0, 8)}...`);
                     localStorage.setItem('userPublicKey', pubkey);
+                    updateClientPubkeyDisplay(pubkey);
+/**
+ * Reset the Nostr login state in case of errors
+ * This can be used to recover from "Already started" errors
+ */
+function resetNostrLoginState(): void {
+  console.log("Resetting Nostr login state...");
+  
+  // Clear our initialization flags
+  window.sessionStorage.removeItem('nostrLoginInitialized');
+  
+  // Clear any auth state that might be stuck
+  ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
+    window.sessionStorage.removeItem(key);
+  });
+  
+  // Clear any relevant localStorage items
+  // (but keep the user's pubkey if they were logged in)
+  ['nostrLoginTemporaryState'].forEach(key => {
+    localStorage.removeItem(key);
+  });
+  
+  // If the NostrLogin library provides a reset method, use it
+  if (NostrLogin && typeof NostrLogin.reset === 'function') {
+    try {
+      NostrLogin.reset();
+      console.log("NostrLogin.reset() called successfully");
+    } catch (error) {
+      console.warn("Error calling NostrLogin.reset():", error);
+    }
+  }
+  
+  console.log("Nostr login state has been reset. You can now try again.");
+}
+
+/**
+ * Ensure user is authenticated before accessing app features
+ * @returns Promise resolving to whether the user is authenticated
+ */
+async function ensureAuthenticated(): Promise<boolean> {
+  // Check if we already have a pubkey in localStorage
+  const savedPubkey = localStorage.getItem('userPublicKey');
+  if (savedPubkey) {
+    console.log(`Found saved pubkey: ${savedPubkey.substring(0, 8)}...`);
+    return true;
+  }
+  
+  // If we don't have a pubkey, check if nostr is available
+  if (!window.nostr) {
+    console.warn("Nostr extension not available");
+    return false;
+  }
+  
+  // Check if auth is already in progress
+  if (window.sessionStorage.getItem('nostrAuthInProgress') === 'true') {
+    console.log("Auth already in progress, resetting state first");
+    resetNostrLoginState();
+    // Wait a moment for the reset to take effect
+    await new Promise(resolve => setTimeout(resolve, 300));
+  }
+  
+  // Set flag that we're starting auth
+  window.sessionStorage.setItem('nostrAuthInProgress', 'true');
+  
+  try {
+    // Try to get the user's public key
+    console.log("Starting Nostr authentication...");
+    const pubkey = await window.nostr.getPublicKey();
+    
+    if (pubkey) {
+      console.log(`Authentication successful, pubkey: ${pubkey.substring(0, 8)}...`);
+      // Store the pubkey for future use
+      localStorage.setItem('userPublicKey', pubkey);
+      // Update UI
+      updateClientPubkeyDisplay(pubkey);
+      return true;
+    }
+    
+    return false;
+  } catch (error) {
+    console.error("Error during authentication:", error);
+    
+    // Handle "Already started" error specifically
+    if (error instanceof Error && error.message === 'Already started') {
+      console.log("Detected 'Already started' error. Resetting login state...");
+      resetNostrLoginState();
+    }
+    
+    return false;
+  } finally {
+    // Clear the in-progress flag regardless of outcome
+    window.sessionStorage.removeItem('nostrAuthInProgress');
+  }
+}
+
                 }).catch(err => {
                     console.warn("Not connected to Nostr extension:", err);
                 });
@@ -563,69 +844,7 @@ async function handlePublishEvent(): Promise<void> {
     }
 }
 
-/**
- * Toggle between light and dark theme
- */
-function toggleTheme(): void {
-    const body = document.body;
-    const themeToggle = document.getElementById('themeToggle');
-    // Commented out to avoid unused variable error
-    // const themeToggleBtn = document.getElementById('themeToggleBtn');
-    const themeIcon = document.getElementById('themeIcon');
-    const themeText = document.getElementById('themeText');
-    
-    const isDarkMode = body.getAttribute('data-theme') === 'dark';
-    
-    if (isDarkMode) {
-        // Switch to light theme
-        body.removeAttribute('data-theme');
-        window.localStorage.setItem('theme', 'light');
-        
-        // Update old toggle if it exists
-        if (themeToggle) {
-            const toggleText = themeToggle.querySelector('.theme-toggle-text');
-            const toggleIcon = themeToggle.querySelector('.theme-toggle-icon');
-            if (toggleText) {
-                toggleText.textContent = 'Dark Mode';
-            }
-            if (toggleIcon) {
-                toggleIcon.textContent = '🌓';
-            }
-        }
-        
-        // Update new toggle button if it exists
-        if (themeIcon) {
-            themeIcon.textContent = '🌙';
-        }
-        if (themeText) {
-            themeText.textContent = 'Dark Mode';
-        }
-    } else {
-        // Switch to dark theme
-        body.setAttribute('data-theme', 'dark');
-        window.localStorage.setItem('theme', 'dark');
-        
-        // Update old toggle if it exists
-        if (themeToggle) {
-            const toggleText = themeToggle.querySelector('.theme-toggle-text');
-            const toggleIcon = themeToggle.querySelector('.theme-toggle-icon');
-            if (toggleText) {
-                toggleText.textContent = 'Light Mode';
-            }
-            if (toggleIcon) {
-                toggleIcon.textContent = '☀️';
-            }
-        }
-        
-        // Update new toggle button if it exists
-        if (themeIcon) {
-            themeIcon.textContent = '☀️';
-        }
-        if (themeText) {
-            themeText.textContent = 'Light Mode';
-        }
-    }
-}
+// toggleTheme function moved to utils.ts
 
 /**
  * Auto-connect to the relay on page load
@@ -656,6 +875,35 @@ async function autoConnectToRelay(): Promise<void> {
  * Handle tab switching
  */
 function setupTabSwitching(): void {
+  // Handle main tab buttons (used in 1120_server.html)
+  const tabButtons = document.querySelectorAll('.tab-button');
+  
+  tabButtons.forEach(button => {
+    button.addEventListener('click', () => {
+      // Remove active class from all tab buttons
+      tabButtons.forEach(btn => btn.classList.remove('active'));
+      
+      // Add active class to clicked tab button
+      button.classList.add('active');
+      
+      // Hide all tab content sections
+      const tabSections = document.querySelectorAll('[id$="-section"]');
+      tabSections.forEach(section => {
+        section.classList.remove('active');
+      });
+      
+      // Show the corresponding tab content
+      const tabId = (button as HTMLElement).dataset.tab;
+      if (tabId) {
+        const tabContent = document.getElementById(tabId);
+        if (tabContent) {
+          tabContent.classList.add('active');
+        }
+      }
+    });
+  });
+  
+  // Also handle .tab-btn and .tab-pane for other parts of the UI
   const tabs = document.querySelectorAll('.tab-btn');
   const tabPanes = document.querySelectorAll('.tab-pane');
   
@@ -732,15 +980,536 @@ function handleCopyEvent(): void {
 }
 
 // Initialize the event handlers when the DOM is loaded
-// Initialize Nostr login as early as possible, before DOM is ready
-initNostrLogin();
+// Handle toggle key format button
+function handleToggleKeyFormat(): void {
+  const toggleKeyFormatBtn = document.getElementById('toggleKeyFormatBtn');
+  const serverPubkeyInput = document.getElementById('serverPubkey') as HTMLInputElement;
+  const keyFormatIndicator = document.getElementById('keyFormatIndicator');
+  
+  if (!toggleKeyFormatBtn || !serverPubkeyInput || !keyFormatIndicator) {
+    return;
+  }
+  
+  let currentFormat = 'npub'; // Default format
+  
+  toggleKeyFormatBtn.addEventListener('click', () => {
+    // Get the current input value
+    const currentValue = serverPubkeyInput.value.trim();
+    if (!currentValue) {
+      return; // Nothing to toggle if empty
+    }
+    
+    try {
+      if (currentFormat === 'npub') {
+        // Switch to hex format
+        // Get the current value from the input
+        const currentValue = serverPubkeyInput.value.trim();
+        
+        // Try multiple approaches to get a valid hex value
+        let hexValue = serverPubkeyInput.dataset.hex;
+        
+        if (!hexValue) {
+          // If no hex stored in data attribute, try to convert from current value
+          if (currentValue.startsWith('npub')) {
+            try {
+              const decoded = nostrTools.nip19.decode(currentValue);
+              if (decoded.type === 'npub') {
+                hexValue = decoded.data as string;
+                console.log('Decoded npub to hex:', hexValue);
+                // Store it for future use
+                serverPubkeyInput.dataset.hex = hexValue;
+                serverPubkeyInput.dataset.npub = currentValue; // Also store the npub
+              }
+            } catch (error) {
+              console.error('Error decoding npub:', error);
+            }
+          }
+          // Check if current value is already a valid hex
+          else if (/^[0-9a-f]{64}$/i.test(currentValue)) {
+            hexValue = currentValue;
+            // Try to also store the npub version for switching back
+            try {
+              const npub = nostrTools.nip19.npubEncode(hexValue);
+              serverPubkeyInput.dataset.npub = npub;
+            } catch (error) {
+              console.error('Error encoding hex to npub:', error);
+            }
+          }
+        }
+        
+        // Only update if we have a valid hex value
+        if (hexValue) {
+          serverPubkeyInput.value = hexValue;
+          currentFormat = 'hex';
+          keyFormatIndicator.textContent = 'Currently showing: HEX format';
+          toggleKeyFormatBtn.textContent = 'NPUB';
+        } else {
+          console.error('Could not determine hex value from:', currentValue);
+        }
+      } else {
+        // Switch to npub format
+        // Get the current value from the input
+        const currentValue = serverPubkeyInput.value.trim();
+        
+        // Try multiple approaches to get a valid npub value
+        let npubValue = serverPubkeyInput.dataset.npub;
+        
+        if (!npubValue) {
+          // If no npub stored in data attribute, try to convert from current value
+          if (/^[0-9a-f]{64}$/i.test(currentValue)) {
+            try {
+              npubValue = nostrTools.nip19.npubEncode(currentValue);
+              // Store it for future use
+              serverPubkeyInput.dataset.npub = npubValue;
+              serverPubkeyInput.dataset.hex = currentValue; // Also store the hex
+            } catch (error) {
+              console.error('Error encoding npub:', error);
+            }
+          }
+        }
+        
+        // Only update if we have a valid npub value
+        if (npubValue) {
+          serverPubkeyInput.value = npubValue;
+          currentFormat = 'npub';
+          keyFormatIndicator.textContent = 'Currently showing: NPUB format';
+          toggleKeyFormatBtn.textContent = 'HEX';
+        }
+      }
+    } catch (error) {
+      console.error('Error toggling key format:', error);
+    }
+  });
+}
 
+/**
+ * Initialize authentication and ensure the user is signed in
+ * This should be called at startup to check authentication status
+ */
+/**
+ * Fixed authentication flow that properly waits for completion
+ * This is a simpler approach that focuses on the specific issue
+ */
+async function safeAuthenticate(): Promise<string | null> {
+  console.log("Starting safe authentication...");
+  
+  // Clear any stale auth state
+  window.sessionStorage.removeItem('nostrLoginInitialized');
+  ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
+    window.sessionStorage.removeItem(key);
+  });
+  
+  // Initialize NostrLogin
+  initNostrLogin();
+  
+  // Wait a moment for initialization to complete
+  await new Promise(resolve => setTimeout(resolve, 300));
+  
+  // If we already have a pubkey in localStorage, use that
+  const savedPubkey = localStorage.getItem('userPublicKey');
+  if (savedPubkey) {
+    console.log(`Found saved pubkey: ${savedPubkey.substring(0, 8)}...`);
+    updateClientPubkeyDisplay(savedPubkey);
+    return savedPubkey;
+  }
+  
+  // Set a flag that we're starting auth
+  window.sessionStorage.setItem('nostrAuthInProgress', 'true');
+  
+  try {
+    // If the extension is available, try to get the pubkey
+    if (window.nostr) {
+      console.log("Requesting public key from extension...");
+      
+      const pubkey = await window.nostr.getPublicKey();
+      if (pubkey) {
+        console.log(`Authentication successful, pubkey: ${pubkey.substring(0, 8)}...`);
+        localStorage.setItem('userPublicKey', pubkey);
+        updateClientPubkeyDisplay(pubkey);
+        return pubkey;
+      }
+    } else {
+      console.warn("Nostr extension not available");
+    }
+    
+    return null;
+  } catch (error) {
+    console.error("Error during authentication:", error);
+    return null;
+  } finally {
+    // Clear the in-progress flag
+    window.sessionStorage.removeItem('nostrAuthInProgress');
+  }
+}
+
+// Initialize authentication as early as possible, before DOM is ready
+// We use an IIFE to allow async/await with the top-level code
+(async function() {
+  try {
+    await safeAuthenticate();
+  } catch (error) {
+    console.error("Error during startup authentication:", error);
+  }
+})();
+
+// Add a reset mechanism for auth state when navigating away from the page
+// This helps ensure we don't get "Already started" errors on page reload
+window.addEventListener('beforeunload', () => {
+  // Clear all auth-related state to ensure a clean start on the next page load
+  window.sessionStorage.removeItem('nostrLoginInitialized');
+  
+  // Clear any temporary auth state in sessionStorage
+  // This is a preventative measure to avoid auth state conflicts
+  ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
+    window.sessionStorage.removeItem(key);
+  });
+});
+
+// Function to manually retry authentication if the user encounters issues
+async function retryAuthentication(): Promise<void> {
+  console.log("Manual authentication retry requested");
+  
+  // Clear UI status
+  const statusElement = document.getElementById('authStatus');
+  if (statusElement) {
+    statusElement.textContent = 'Retrying authentication...';
+    statusElement.className = 'status-message loading';
+  }
+  
+  // Clear all auth state
+  window.sessionStorage.removeItem('nostrLoginInitialized');
+  ['nostrAuthInProgress', 'nostrLoginState', 'nostrAuthPending', 'nostrLoginStarted'].forEach(key => {
+    window.sessionStorage.removeItem(key);
+  });
+  
+  // Wait a moment for state to clear
+  await new Promise(resolve => setTimeout(resolve, 300));
+  
+  // Try authentication again
+  const pubkey = await safeAuthenticate();
+  
+  // Update status
+  if (statusElement) {
+    if (pubkey) {
+      statusElement.textContent = 'Authentication successful!';
+      statusElement.className = 'status-message success';
+    } else {
+      statusElement.textContent = 'Authentication failed. Please check your Nostr extension.';
+      statusElement.className = 'status-message error';
+    }
+  }
+}
+
+// Initialize HTTP response viewer with raw/formatted view support and 21121 integration
+window.addEventListener('DOMContentLoaded', () => {
+  try {
+    import('./http-response-viewer').then(module => {
+      module.initHttpResponseViewer();
+      console.log('HTTP response viewer initialized successfully');
+    }).catch(e => {
+      console.error('Failed to initialize HTTP response viewer:', e);
+    });
+  } catch (e) {
+    console.error('Error loading HTTP response viewer module:', e);
+  }
+});
+
+/**
+ * Handle parsing of raw Nostr events from text input
+ */
+async function handleParseRawEvent(): Promise<void> {
+  const rawEventInput = document.getElementById('rawEventInput') as HTMLTextAreaElement;
+  const rawInputStatus = document.getElementById('rawInputStatus');
+  
+  if (!rawEventInput || !rawInputStatus) {
+    console.error('Required elements not found');
+    return;
+  }
+  
+  const rawText = rawEventInput.value.trim();
+  if (!rawText) {
+    rawInputStatus.textContent = 'Please enter a raw Nostr event JSON';
+    rawInputStatus.className = 'status-message error';
+    return;
+  }
+  
+  try {
+    // Try to parse the JSON
+    let eventData;
+    try {
+      eventData = JSON.parse(rawText);
+    } catch (error) {
+      rawInputStatus.textContent = 'Invalid JSON format';
+      rawInputStatus.className = 'status-message error';
+      return;
+    }
+    
+    // Validate that it's a proper Nostr event
+    if (!eventData || typeof eventData !== 'object') {
+      rawInputStatus.textContent = 'Invalid event object';
+      rawInputStatus.className = 'status-message error';
+      return;
+    }
+    
+    // Check for required Nostr event fields
+    if (!eventData.id || !eventData.pubkey || !eventData.kind || eventData.created_at === undefined) {
+      rawInputStatus.textContent = 'Missing required Nostr event fields (id, pubkey, kind, created_at)';
+      rawInputStatus.className = 'status-message error';
+      return;
+    }
+    
+    // Check if it's a kind 21120 event
+    if (eventData.kind !== 21120) {
+      rawInputStatus.textContent = 'Warning: This is not a kind 21120 event. It may not display properly.';
+      rawInputStatus.className = 'status-message warning';
+    } else {
+      rawInputStatus.textContent = 'Valid kind 21120 event parsed successfully';
+      rawInputStatus.className = 'status-message success';
+    }
+    
+    // Add the event to the UI
+    try {
+      // Add the event to the events list using existing UI services
+      const eventsList = document.getElementById('eventsList');
+      if (eventsList) {
+        // Clear the "No events" message if it exists
+        const emptyState = eventsList.querySelector('.empty-state');
+        if (emptyState) {
+          (emptyState as HTMLElement).style.display = 'none';
+        }
+        
+        // Create event item
+        const eventItem = document.createElement('div');
+        eventItem.className = 'event-item';
+        eventItem.dataset.id = eventData.id;
+        
+        // Format timestamp
+        const date = new Date(eventData.created_at * 1000);
+        const timeString = date.toLocaleTimeString();
+        
+        // Set event content
+        eventItem.innerHTML = `
+          <div class="event-header">
+            <div class="event-time">${timeString}</div>
+            <div class="event-kind">Kind: 21120</div>
+          </div>
+          <div class="event-content-preview">
+            ${eventData.content.substring(0, 50)}${eventData.content.length > 50 ? '...' : ''}
+          </div>
+        `;
+        
+        // Add click handler
+        eventItem.addEventListener('click', () => {
+          // Find all event items and remove 'selected' class
+          const allItems = eventsList.querySelectorAll('.event-item');
+          allItems.forEach(item => item.classList.remove('selected'));
+          
+          // Add 'selected' class to this item
+          eventItem.classList.add('selected');
+          
+          // Show event details
+          const eventDetails = document.getElementById('eventDetails');
+          if (eventDetails) {
+            // Clear any "select an event" message
+            const emptyState = eventDetails.querySelector('.empty-state');
+            if (emptyState) {
+              (emptyState as HTMLElement).style.display = 'none';
+            }
+            
+            // Format HTTP content for display
+            let contentDisplay = '';
+            try {
+              // Use the imported HttpFormatter directly with the correct parameters
+              contentDisplay = HttpFormatter.formatHttpContent(eventData.content, true, false);
+            } catch (e) {
+              contentDisplay = `<pre>${eventData.content}</pre>`;
+            }
+            
+            // Display event details
+            eventDetails.innerHTML = `
+              <div class="event-detail-header">
+                <h3>Event ID: ${eventData.id.substring(0, 8)}...</h3>
+                <div class="event-detail-metadata">
+                  <div>Kind: 21120</div>
+                  <div>Created: ${date.toLocaleString()}</div>
+                  <div>Author: ${eventData.pubkey.substring(0, 8)}...</div>
+                </div>
+              </div>
+              <div class="event-detail-content">
+                <h4>HTTP Content:</h4>
+                <div class="http-content-container">
+                  ${contentDisplay}
+                </div>
+              </div>
+              <div class="event-detail-tags">
+                <h4>Tags:</h4>
+                <div class="tags-container">
+                  ${eventData.tags.map((tag: string[]) => `
+                    <div class="tag-item">
+                      <span class="tag-name">${tag[0]}</span>
+                      <span class="tag-value">${tag[1] || ''}</span>
+                    </div>
+                  `).join('')}
+                </div>
+              </div>
+            `;
+          }
+        });
+        
+        // Add to the events list
+        eventsList.appendChild(eventItem);
+        
+        // Trigger a click to show the details
+        eventItem.click();
+      }
+    } catch (error) {
+      console.error('Error processing event:', error);
+      rawInputStatus.textContent = `Error processing event: ${error instanceof Error ? error.message : String(error)}`;
+      rawInputStatus.className = 'status-message error';
+    }
+  } catch (error) {
+    console.error('Error handling raw event:', error);
+    rawInputStatus.textContent = `Error: ${error instanceof Error ? error.message : String(error)}`;
+    rawInputStatus.className = 'status-message error';
+  }
+}
+
+/**
+ * Handle toggle client key format button
+ */
+function handleToggleClientKeyFormat(): void {
+  const toggleClientKeyFormatBtn = document.getElementById('toggleClientKeyFormatBtn');
+  const clientPubkeyElement = document.getElementById('clientPubkey');
+  const clientKeyFormatIndicator = document.getElementById('clientKeyFormatIndicator');
+  
+  if (!toggleClientKeyFormatBtn || !clientPubkeyElement || !clientKeyFormatIndicator) {
+    return;
+  }
+  
+  let currentFormat = 'npub'; // Default format
+  
+  toggleClientKeyFormatBtn.addEventListener('click', () => {
+    // Get the current text content
+    const currentValue = clientPubkeyElement.textContent?.trim() || '';
+    if (!currentValue || currentValue === 'Not logged in') {
+      return; // Nothing to toggle if empty or not logged in
+    }
+    
+    try {
+      if (currentFormat === 'npub') {
+        // Switch to hex format
+        let hexValue = clientPubkeyElement.dataset.hex;
+        
+        if (hexValue) {
+          clientPubkeyElement.textContent = hexValue;
+          currentFormat = 'hex';
+          clientKeyFormatIndicator.textContent = 'Currently showing: HEX format';
+          toggleClientKeyFormatBtn.textContent = 'NPUB';
+        }
+      } else {
+        // Switch to npub format
+        let npubValue = clientPubkeyElement.dataset.npub;
+        
+        if (npubValue) {
+          clientPubkeyElement.textContent = npubValue;
+          currentFormat = 'npub';
+          clientKeyFormatIndicator.textContent = 'Currently showing: NPUB format';
+          toggleClientKeyFormatBtn.textContent = 'HEX';
+        }
+      }
+    } catch (error) {
+      console.error('Error toggling client key format:', error);
+    }
+  });
+}
+
+// Navbar initialization is now handled by the navbar-init module
+// But we'll add an explicit initialization here as well for redundancy
 document.addEventListener('DOMContentLoaded', function(): void {
+  console.log('%c[Client] DOMContentLoaded event - ensuring navbar is initialized', 'background: #4CAF50; color: white');
+  
+  // Try to initialize the navbar directly from this file too
+  try {
+    initializeNavbar();
+  } catch (error) {
+    console.error('[Client] Error initializing navbar:', error);
+  }
   // Load previously selected server, if any
   loadSavedServer();
   
+  // Initialize the key format toggle functionality
+  handleToggleKeyFormat();
+  
+  // Initialize the client key format toggle functionality
+  handleToggleClientKeyFormat();
+  
+  // Try to get and display client pubkey immediately when DOM is loaded
+  getUserPubkey().then(pubkey => {
+    if (pubkey) {
+      updateClientPubkeyDisplay(pubkey);
+    }
+  });
+  
+  // Add auth status indicator and retry button to the UI
+  const addAuthUI = (): void => {
+    // Find a good place for the auth elements (navbar or header)
+    const navbarRight = document.querySelector('.nav-right, header');
+    if (navbarRight) {
+      // Create container for auth elements
+      const authContainer = document.createElement('div');
+      authContainer.className = 'auth-container';
+      authContainer.style.display = 'flex';
+      authContainer.style.alignItems = 'center';
+      authContainer.style.marginLeft = 'auto';
+      authContainer.style.marginRight = '10px';
+      
+      // Create status indicator
+      const authStatus = document.createElement('div');
+      authStatus.id = 'authStatus';
+      authStatus.className = 'status-message';
+      authStatus.style.fontSize = '0.8em';
+      authStatus.style.marginRight = '10px';
+      
+      // Check current auth status
+      const pubkey = localStorage.getItem('userPublicKey');
+      if (pubkey) {
+        authStatus.textContent = 'Authenticated';
+        authStatus.className = 'status-message success';
+      } else {
+        authStatus.textContent = 'Not authenticated';
+        authStatus.className = 'status-message warning';
+      }
+      
+      // Create retry button
+      const retryButton = document.createElement('button');
+      retryButton.textContent = 'Retry Auth';
+      retryButton.className = 'auth-retry-btn';
+      retryButton.style.fontSize = '0.8em';
+      retryButton.style.padding = '3px 8px';
+      retryButton.addEventListener('click', async () => {
+        await retryAuthentication();
+      });
+      
+      // Add elements to container
+      authContainer.appendChild(authStatus);
+      authContainer.appendChild(retryButton);
+      
+      // Add container to navbar
+      navbarRight.appendChild(authContainer);
+    }
+  };
+  
+  // Add auth UI after a short delay to ensure other elements are loaded
+  setTimeout(addAuthUI, 500);
+  
   // Auto-connect to the relay on page load
   setTimeout(autoConnectToRelay, 500);
+  
+  // Initialize raw event parsing functionality
+  const parseRawEventBtn = document.getElementById('parseRawEventBtn');
+  if (parseRawEventBtn) {
+    parseRawEventBtn.addEventListener('click', handleParseRawEvent);
+  }
   // Search relay button event listener
   const searchRelayBtn = document.getElementById('searchRelayBtn');
   if (searchRelayBtn) {
@@ -821,45 +1590,6 @@ document.addEventListener('DOMContentLoaded', function(): void {
     // Set default HTTP request
     setDefaultHttpRequest();
     
-    // Initialize theme toggle
-    const themeToggle = document.getElementById('themeToggle');
-    const themeToggleBtn = document.getElementById('themeToggleBtn');
-    
-    // First try the new button, then fall back to the old toggle
-    const toggleElement = themeToggleBtn || themeToggle;
-    
-    if (toggleElement) {
-        // Set initial theme based on local storage
-        const savedTheme = window.localStorage.getItem('theme');
-        if (savedTheme === 'dark') {
-            document.body.setAttribute('data-theme', 'dark');
-            
-            // Update UI for whichever toggle we're using
-            if (themeToggleBtn) {
-                const themeIcon = document.getElementById('themeIcon');
-                const themeText = document.getElementById('themeText');
-                if (themeIcon) {
-                    themeIcon.textContent = '☀️';
-                }
-                if (themeText) {
-                    themeText.textContent = 'Light Mode';
-                }
-            } else if (themeToggle) {
-                const toggleText = themeToggle.querySelector('.theme-toggle-text');
-                const toggleIcon = themeToggle.querySelector('.theme-toggle-icon');
-                if (toggleText) {
-                    toggleText.textContent = 'Light Mode';
-                }
-                if (toggleIcon) {
-                    toggleIcon.textContent = '☀️';
-                }
-            }
-        }
-        
-        // Add click handler
-        toggleElement.addEventListener('click', toggleTheme);
-    }
-    
     // Initialize copy button
     handleCopyEvent();
     
diff --git a/client/src/converter.ts b/client/src/converter.ts
index 1121e47..15a929d 100644
--- a/client/src/converter.ts
+++ b/client/src/converter.ts
@@ -6,6 +6,7 @@ import qrcode from 'qrcode-generator';
 import { defaultServerConfig, appSettings } from './config';
 import { convertNpubToHex } from './relay';
 import { showSuccess } from './utils';
+import { encryptWithWebCrypto, encryptKeyWithNostrExtension } from './utils/crypto-utils';
 
 // Define interface for Nostr event - making id and sig optional for event creation
 export interface NostrEvent {
@@ -25,8 +26,8 @@ interface NostrWindowExtension {
     getPublicKey: () => Promise<string>;
     signEvent: (event: NostrEvent) => Promise<NostrEvent>;
     nip44?: {
-        encrypt(pubkey: string, plaintext: string): Promise<string>;
-        decrypt(pubkey: string, ciphertext: string): Promise<string>;
+        encrypt(plaintext: string, pubkey: string): Promise<string>;
+        decrypt(ciphertext: string, pubkey: string): Promise<string>;
     };
 }
 
@@ -70,47 +71,7 @@ function initStandaloneKeypair(): { publicKey: string, secretKey: Uint8Array } {
  * @param relay Optional relay for the response
  * @returns Stringified Nostr event or null if conversion fails
  */
-// Helper function for proper Web Crypto API encryption
-async function encryptWithWebCrypto(data: string, key: string): Promise<string> {
-    // Convert text to bytes
-    const dataBytes = new TextEncoder().encode(data);
-    
-    // Create a key from the provided key (using SHA-256 hash)
-    const keyMaterial = await crypto.subtle.digest(
-        'SHA-256',
-        new TextEncoder().encode(key)
-    );
-    
-    // Import the key
-    const cryptoKey = await crypto.subtle.importKey(
-        'raw',
-        keyMaterial,
-        { name: 'AES-GCM' },
-        false,
-        ['encrypt']
-    );
-    
-    // Generate random IV
-    const iv = crypto.getRandomValues(new Uint8Array(12));
-    
-    // Encrypt the data
-    const encryptedData = await crypto.subtle.encrypt(
-        {
-            name: 'AES-GCM',
-            iv: iv
-        },
-        cryptoKey,
-        dataBytes
-    );
-    
-    // Combine IV and ciphertext
-    const encryptedArray = new Uint8Array(iv.length + encryptedData.byteLength);
-    encryptedArray.set(iv);
-    encryptedArray.set(new Uint8Array(encryptedData), iv.length);
-    
-    // Convert to base64 string
-    return btoa(String.fromCharCode.apply(null, Array.from(encryptedArray)));
-}
+// Encryption functions now imported from crypto-utils.ts
 
 export async function convertToEvent(
     httpRequest: string,
@@ -145,10 +106,7 @@ export async function convertToEvent(
             }
         }
         
-        // Validate that we have a hex string of the right length
-        if (!/^[0-9a-f]{64}$/i.test(serverPubkeyHex)) {
-            throw new Error("Invalid server pubkey format. Must be a 64-character hex string.");
-        }
+        // No need to validate the hex string length - just assume it's valid at this point
         // Encrypt the HTTP request directly without wrapping it in JSON
         // This ensures the decrypted content is the raw HTTP request
         console.log("Encrypting raw HTTP request with Web Crypto API");
@@ -168,10 +126,10 @@ export async function convertToEvent(
     // Ensure the serverPubkey is in valid hex format for the p tag
     let pTagValue = serverPubkey;
 
-    // First check if it's already a valid hex string
-    if (/^[0-9a-f]{64}$/i.test(serverPubkey)) {
-        // Already a valid hex pubkey, use as is
-        console.log(`Server pubkey is already a valid hex string: ${serverPubkey}`);
+    // First check if it's already a valid hex string (not npub)
+    if (!serverPubkey.startsWith('npub')) {
+        // Assume it's a hex pubkey
+        console.log(`Server pubkey is assumed to be hex format: ${serverPubkey}`);
     }
     // Try to convert from npub format if needed
     else if (serverPubkey.startsWith('npub')) {
@@ -199,41 +157,40 @@ export async function convertToEvent(
     const finalContent = typeof encryptedContent === 'string' ?
         encryptedContent : JSON.stringify(encryptedContent);
         
-    // Encrypt the decryption key using NIP-44 through nostr-signers
+    // Encrypt the decryption key using NIP-44
     let encryptedKey = decryptkey;
     
-    // First ensure we're properly connected to nostr-signers
-    if (!window.nostr) {
-        console.warn("window.nostr not available - ensure a NIP-07 extension is installed and connected");
-    } else if (typeof window.nostr.nip44 !== 'object' || typeof window.nostr.nip44.encrypt !== 'function') {
-        console.warn("NIP-44 encryption not available - connect to a compatible NIP-07 extension");
+    try {
+        console.log("Encrypting decryption key using NIP-44 via crypto-utils");
         
-        // Log additional diagnostic information
-        console.log("Available nostr methods:", Object.keys(window.nostr).join(", "));
-        
-        if (NostrLogin && typeof NostrLogin.getPublicKey === 'function') {
+        // Ensure server pubkey is in npub format for validation
+        let serverNpub = "";
+        if (pTagValue.startsWith('npub')) {
+            // Already in npub format
+            serverNpub = pTagValue;
+            console.log(`Using npub format for encryption: ${serverNpub.substring(0, 12)}...`);
+        } else {
+            // Assume it's hex if not npub
             try {
-                // Try to explicitly connect using NostrLogin
-                const pubkey = await NostrLogin.getPublicKey();
-                console.log("Retrieved public key via NostrLogin:", pubkey);
-                // Sometimes this explicit call triggers the extension to connect properly
-            } catch (e) {
-                console.error("Failed to connect via NostrLogin:", e);
+                serverNpub = nostrTools.nip19.npubEncode(pTagValue);
+                console.log(`Converted hex pubkey to npub for NIP-44 encryption: ${serverNpub}`);
+            } catch (encodeError) {
+                console.error("Error encoding hex to npub:", encodeError);
+                throw new Error(`Failed to encode pubkey to npub format: ${pTagValue}`);
             }
         }
-    } else {
-        try {
-            console.log("Encrypting decryption key using window.nostr.nip44.encrypt");
-            // According to the NIP-07 spec, the first parameter is the pubkey (recipient)
-            // and the second parameter is the plaintext to encrypt
-            const encryptionPromise = window.nostr.nip44.encrypt(pTagValue, decryptkey);
-            // Since this is a Promise, we need to await it
-            encryptedKey = await encryptionPromise;
-            console.log("Successfully encrypted the decryption key with NIP-44");
-        } catch (encryptError) {
-            console.error("Failed to encrypt key with NIP-44:", encryptError instanceof Error ? encryptError.message : String(encryptError));
-            console.log("Using unencrypted key as fallback");
-        }
+        
+        // Log the parameters being used
+        console.log(`Using parameters for NIP-44 encryption:
+          - plaintext: ${decryptkey.substring(0, 3)}...${decryptkey.substring(decryptkey.length - 3)}
+          - pubkey: ${serverNpub.substring(0, 12)}...`);
+          
+        // Use our utility function again
+        encryptedKey = await encryptKeyWithNostrExtension(decryptkey, serverNpub);
+        console.log("Successfully encrypted the decryption key with NIP-44");
+    } catch (encryptError) {
+        console.error("Failed to encrypt key with NIP-44:", encryptError instanceof Error ? encryptError.message : String(encryptError));
+        console.log("Using unencrypted key as fallback");
     }
     
     const event: NostrEvent = {
@@ -276,10 +233,10 @@ export async function convertToEvent(
                 }
             }
             
-            // Verify the tag is now in hex format
-            if (!/^[0-9a-f]{64}$/i.test(event.tags[i][1])) {
-                console.error(`Invalid hex format in p tag: ${event.tags[i][1]}`);
-                throw new Error(`P tag must be a 64-character hex string, got: ${event.tags[i][1]}`);
+            // Verify the tag is now in hex format (not npub)
+            if (event.tags[i][1].startsWith('npub')) {
+                console.error(`P tag still in npub format: ${event.tags[i][1]}`);
+                throw new Error(`P tag must be in hex format, but is still in npub format: ${event.tags[i][1]}`);
             }
         }
     }
@@ -404,23 +361,25 @@ export async function displayConvertedEvent(): Promise<void> {
             let signedEvent;
 
             try {
-                // Try to sign with NostrLogin in preferred order
-                if (NostrLogin && NostrLogin.signEvent) {
-                    console.log("Using NostrLogin.signEvent to sign event");
-                    signedEvent = await NostrLogin.signEvent(nostrEvent);
-                } else if (NostrLogin && NostrLogin.sign) {
-                    console.log("Using NostrLogin.sign to sign event");
-                    signedEvent = await NostrLogin.sign(nostrEvent);
-                } else if (window.nostr) {
-                    // Fall back to NIP-07 extension
+                // Always prioritize the user's identity via NIP-07 extension
+                if (window.nostr) {
+                    // Use the NIP-07 extension - this ensures we're using the USER identity
                     console.log("Using NIP-07 extension to sign event");
                     // When using the extension directly, ensure content is a string
                     if (typeof nostrEvent.content !== 'string') {
                         nostrEvent.content = String(nostrEvent.content);
                     }
                     signedEvent = await window.nostr.signEvent(nostrEvent);
+                }
+                // Fall back to NostrLogin only if window.nostr isn't available
+                else if (NostrLogin && NostrLogin.signEvent) {
+                    console.log("Using NostrLogin.signEvent to sign event");
+                    signedEvent = await NostrLogin.signEvent(nostrEvent);
+                } else if (NostrLogin && NostrLogin.sign) {
+                    console.log("Using NostrLogin.sign to sign event");
+                    signedEvent = await NostrLogin.sign(nostrEvent);
                 } else if (secretKey) {
-                    // Fall back to nostr-tools
+                    // Last resort - fall back to nostr-tools
                     console.log("Using nostr-tools to sign event");
                     signedEvent = nostrTools.finalizeEvent(nostrEvent, secretKey);
                 } else {
diff --git a/client/src/http-response-viewer.ts b/client/src/http-response-viewer.ts
new file mode 100644
index 0000000..b80ef20
--- /dev/null
+++ b/client/src/http-response-viewer.ts
@@ -0,0 +1,318 @@
+/**
+ * HTTP Response Viewer module
+ * Handles displaying HTTP responses and 21121 integration
+ */
+import { NostrEvent } from './relay';
+import { HttpFormatter } from './services/HttpFormatter';
+import { ToastNotifier } from './services/ToastNotifier';
+
+/**
+ * Initialize the HTTP response viewer functionality
+ */
+export function initHttpResponseViewer(): void {
+  console.log('Initializing HTTP response viewer...');
+
+  // Add event listener for tab switching in the HTTP response modal
+  document.addEventListener('click', (event) => {
+    const target = event.target as HTMLElement;
+    
+    // Handle tab switching
+    if (target && target.classList.contains('tab-btn')) {
+      const tabContainer = target.closest('.http-response-tabs, .event-detail-tabs');
+      if (!tabContainer) return;
+      
+      // Get all tab buttons and content in this container
+      const tabButtons = tabContainer.querySelectorAll('.tab-btn');
+      
+      // Find the tab content container (parent or sibling depending on structure)
+      let tabContentContainer = tabContainer.nextElementSibling;
+      if (!tabContentContainer || !tabContentContainer.querySelector('.tab-content')) {
+        // If not a sibling, try to find a parent that contains the tab content
+        tabContentContainer = tabContainer.closest('.modal-content, .event-details');
+      }
+      
+      if (!tabContentContainer) return;
+      
+      const tabContents = tabContentContainer.querySelectorAll('.tab-content');
+      
+      // Remove active class from all tabs and content
+      tabButtons.forEach(btn => btn.classList.remove('active'));
+      tabContents.forEach(content => content.classList.remove('active'));
+      
+      // Add active class to clicked tab
+      target.classList.add('active');
+      
+      // Find the corresponding content
+      const tabId = target.getAttribute('data-tab');
+      if (tabId) {
+        const tabContent = document.getElementById(tabId) || 
+                           tabContentContainer.querySelector(`#${tabId}`);
+        if (tabContent) {
+          tabContent.classList.add('active');
+        }
+      }
+    }
+    
+    // Handle execute HTTP request button
+    if (target && (
+      target.classList.contains('execute-http-request-btn') || 
+      target.closest('.execute-http-request-btn')
+    )) {
+      // Prevent multiple clicks
+      const button = target.classList.contains('execute-http-request-btn') ? 
+                    target : target.closest('.execute-http-request-btn');
+      
+      if (button && !button.hasAttribute('disabled')) {
+        executeHttpRequest(button as HTMLElement);
+      }
+    }
+    
+    // Handle close modal button
+    if (target && (
+      target.classList.contains('close-modal-btn') || 
+      target.closest('.close-modal-btn')
+    )) {
+      const modal = target.closest('.http-response-modal');
+      if (modal) {
+        (modal as HTMLElement).style.display = 'none';
+      }
+    }
+    
+    // Handle clicking outside the modal to close it
+    if (target && target.classList.contains('http-response-modal')) {
+      (target as HTMLElement).style.display = 'none';
+    }
+  });
+}
+
+/**
+ * Execute an HTTP request from a button click
+ * @param button The button element that was clicked
+ */
+async function executeHttpRequest(button: HTMLElement): Promise<void> {
+  // Store the original button text
+  const originalText = button.textContent || 'Execute HTTP Request';
+  
+  // Update button to show it's working
+  button.textContent = 'Executing...';
+  button.setAttribute('disabled', 'true');
+  
+  try {
+    // Find the HTTP content
+    const eventDetails = button.closest('.event-details');
+    if (!eventDetails) throw new Error('Event details not found');
+    
+    // Find the HTTP content element - look in both formatted and raw tabs
+    const httpContentElement = 
+      eventDetails.querySelector('#raw-http .http-content') || 
+      eventDetails.querySelector('.http-content');
+    
+    if (!httpContentElement) throw new Error('HTTP content not found');
+    
+    const httpContent = httpContentElement.textContent || '';
+    if (!httpContent.trim()) throw new Error('Empty HTTP content');
+    
+    // Get the event ID
+    const headerElement = eventDetails.querySelector('.event-detail-header h3');
+    const eventIdMatch = headerElement?.textContent?.match(/ID: (\w+)\.\.\./);
+    const eventId = eventIdMatch ? eventIdMatch[1] : null;
+    
+    if (!eventId) throw new Error('Could not determine event ID');
+    
+    // Execute the HTTP request
+    const response = await executeRequest(httpContent);
+    
+    // Display the response 
+    displayHttpResponse(response);
+    
+    // Ask if the user wants to create a 21121 response event
+    if (confirm('Do you want to create and publish a NIP-21121 response event?')) {
+      await createAndPublish21121Response(eventId, response);
+    }
+  } catch (error) {
+    // Show error
+    console.error('Error executing HTTP request:', error);
+    ToastNotifier.show(`Error: ${error instanceof Error ? error.message : String(error)}`, 'error');
+    
+    const errorResponse = `HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\n\r\nError: ${error instanceof Error ? error.message : String(error)}`;
+    displayHttpResponse(errorResponse);
+  } finally {
+    // Restore button state
+    button.textContent = originalText;
+    button.removeAttribute('disabled');
+  }
+}
+
+/**
+ * Execute an HTTP request
+ * @param requestContent The HTTP request content
+ * @returns The HTTP response content
+ */
+async function executeRequest(requestContent: string): Promise<string> {
+  // Parse the request
+  const lines = requestContent.trim().split('\n');
+  if (lines.length === 0) {
+    throw new Error('Empty request');
+  }
+
+  // Parse the first line (e.g., "GET /api/users HTTP/1.1")
+  const firstLine = lines[0].trim();
+  const [method, path, _httpVersion] = firstLine.split(' ');
+
+  // Extract the host
+  let host = '';
+  let headers: Record<string, string> = {};
+  let body = '';
+  let inHeaders = true;
+
+  for (let i = 1; i < lines.length; i++) {
+    const line = lines[i].trim();
+    
+    if (inHeaders && line === '') {
+      inHeaders = false;
+      continue;
+    }
+    
+    if (inHeaders) {
+      const colonIndex = line.indexOf(':');
+      if (colonIndex > 0) {
+        const key = line.substring(0, colonIndex).trim();
+        const value = line.substring(colonIndex + 1).trim();
+        headers[key.toLowerCase()] = value;
+        
+        if (key.toLowerCase() === 'host') {
+          host = value;
+        }
+      }
+    } else {
+      body += line + '\n';
+    }
+  }
+
+  if (!host) {
+    throw new Error('Host header is required');
+  }
+
+  // Construct URL
+  const isSecure = host.includes(':443') || !host.includes(':');
+  const protocol = isSecure ? 'https://' : 'http://';
+  const baseUrl = protocol + host.split(':')[0];
+  const url = new URL(path, baseUrl).toString();
+
+  // Perform fetch
+  try {
+    const response = await fetch(url, {
+      method,
+      headers,
+      body: body.trim() || undefined,
+    });
+
+    // Build response string
+    let responseText = `HTTP/1.1 ${response.status} ${response.statusText}\n`;
+    
+    // Add headers
+    response.headers.forEach((value, key) => {
+      responseText += `${key}: ${value}\n`;
+    });
+    
+    // Add body
+    const responseBody = await response.text();
+    responseText += '\n' + responseBody;
+    
+    return responseText;
+  } catch (error) {
+    return `HTTP/1.1 500 Internal Server Error\nContent-Type: text/plain\n\nError: ${error instanceof Error ? error.message : String(error)}`;
+  }
+}
+
+/**
+ * Display HTTP response in the modal
+ * @param response The HTTP response content
+ */
+function displayHttpResponse(response: string): void {
+  // Get the modal
+  const modal = document.getElementById('httpResponseModal');
+  if (!modal) {
+    console.error('HTTP response modal not found');
+    return;
+  }
+  
+  // Update the modal content
+  const formattedContainer = modal.querySelector('#formatted-response .http-formatted-container');
+  const rawContainer = modal.querySelector('#raw-response pre');
+  
+  if (formattedContainer) {
+    formattedContainer.innerHTML = HttpFormatter.formatHttpContent(response, false, true);
+  }
+  
+  if (rawContainer) {
+    rawContainer.textContent = response;
+  }
+  
+  // Show the modal
+  (modal as HTMLElement).style.display = 'block';
+}
+
+/**
+ * Create and publish a 21121 response event
+ * @param requestEventId The ID of the 21120 request event
+ * @param responseContent The HTTP response content
+ */
+async function createAndPublish21121Response(requestEventId: string, responseContent: string): Promise<void> {
+  try {
+    // Get the server's private key
+    const serverNsec = localStorage.getItem('serverNsec');
+    if (!serverNsec) {
+      ToastNotifier.show('Server private key (nsec) not found. Please set up a server identity first.', 'error');
+      return;
+    }
+
+    // We need to import the NostrService dynamically to avoid circular dependencies
+    const { NostrService } = await import('./services/NostrService');
+    const { Nostr21121Service } = await import('./services/Nostr21121Service');
+    
+    // Create temporary services if needed
+    const nostrService = new NostrService();
+    const relayService = nostrService.getRelayService();
+    const cacheService = nostrService.getCacheService();
+    const nostr21121Service = new Nostr21121Service(relayService, cacheService);
+    
+    // Get the relay URL from the UI
+    const relayUrlInput = document.getElementById('relayUrl') as HTMLInputElement;
+    const relayUrl = relayUrlInput?.value || 'wss://relay.degmods.com'; 
+    
+    // First get the original request event
+    const originalEvent = await nostrService.getEventById(relayUrl, requestEventId);
+    
+    if (!originalEvent) {
+      ToastNotifier.show(`Could not find original request event with ID: ${requestEventId}`, 'error');
+      return;
+    }
+    
+    // Create and publish the 21121 event
+    const responseEvent = await nostr21121Service.createAndPublish21121Event(
+      originalEvent,
+      responseContent,
+      serverNsec,
+      relayUrl
+    );
+    
+    if (responseEvent) {
+      ToastNotifier.show('NIP-21121 response event published successfully!', 'success');
+      
+      // Add a visual indicator to the event in the UI
+      const eventItem = document.querySelector(`.event-item[data-id="${requestEventId}"]`);
+      if (eventItem && !eventItem.querySelector('.response-indicator')) {
+        const responseIndicator = document.createElement('div');
+        responseIndicator.className = 'response-indicator';
+        responseIndicator.innerHTML = '<span class="response-available">21121 Response Available</span>';
+        eventItem.appendChild(responseIndicator);
+      }
+    } else {
+      ToastNotifier.show('Failed to publish NIP-21121 response event', 'error');
+    }
+  } catch (error) {
+    console.error('Error creating 21121 response:', error);
+    ToastNotifier.show(`Error: ${error instanceof Error ? error.message : String(error)}`, 'error');
+  }
+}
\ No newline at end of file
diff --git a/client/src/navbar-init.ts b/client/src/navbar-init.ts
new file mode 100644
index 0000000..c0c5c76
--- /dev/null
+++ b/client/src/navbar-init.ts
@@ -0,0 +1,238 @@
+/**
+ * navbar-init.ts
+ * Standalone module for initializing the navigation bar
+ * This module self-executes when loaded
+ */
+// Use the theme utilities from the dedicated file
+import * as themeUtils from './theme-utils';
+
+
+/**
+ * Function to initialize the navbar content and functionality
+ */
+function initializeNavbar(): void {
+    console.log('[NAVBAR-INIT] Starting navbar initialization');
+    
+    // Get the current page URL to highlight the correct link
+    const currentPageUrl = window.location.pathname;
+    const currentPage = currentPageUrl.split('/').pop() || 'index.html';
+    console.log('[NAVBAR-INIT] Current page:', currentPage);
+    
+    // Find the navbar container
+    const navbarContainer = document.getElementById('navbarContainer');
+    console.log('[NAVBAR-INIT] Navbar container found:', !!navbarContainer);
+    
+    if (navbarContainer) {
+        // Create the navbar HTML based on current page
+        const navbarHtml = `
+        <div class="nav-left">
+            <a href="./index.html" class="nav-link${currentPage === 'index.html' ? ' active' : ''}">HOME</a>
+            <a href="./1120_client.html" class="nav-link${currentPage === '1120_client.html' ? ' active' : ''}">CLIENT</a>
+            <a href="./1120_server.html" class="nav-link${currentPage === '1120_server.html' ? ' active' : ''}">SERVER</a>
+            <a href="./billboard.html" class="nav-link${currentPage === 'billboard.html' ? ' active' : ''}">BILLBOARD</a>
+        </div>
+        <div class="nav-right">
+            <a href="./index.html" class="nav-link nav-icon${currentPage === 'index.html' ? ' active' : ''}" title="Documentation">❓</a>
+            <a href="./profile.html" class="nav-link nav-icon${currentPage === 'profile.html' ? ' active' : ''}" title="Profile">👤</a>
+            <button id="nuclearResetBtn" class="nuclear-reset-btn" title="Reset All Data">💣</button>
+            <button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Dark Mode">
+                <span id="themeIcon">🌙</span>
+            </button>
+        </div>`;
+        
+        // Update the navbar container
+        console.log('[NAVBAR-INIT] Setting navbar HTML content');
+        navbarContainer.innerHTML = navbarHtml;
+        
+        // Setup theme toggle and nuclear reset button
+        setupThemeToggle();
+        setupNuclearReset();
+    } else {
+        console.error('[NAVBAR-INIT] Navbar container not found!');
+    }
+}
+
+/**
+ * Function to set up the theme toggle button
+ */
+function setupThemeToggle(): void {
+    console.log('[NAVBAR-INIT] Setting up theme toggle');
+    
+    // Get the theme toggle button
+    const themeToggleBtn = document.getElementById('themeToggleBtn');
+    console.log('[NAVBAR-INIT] Theme toggle button found:', !!themeToggleBtn);
+    
+    if (themeToggleBtn) {
+        // Set initial theme based on localStorage
+        const savedTheme = localStorage.getItem('theme');
+        console.log('[NAVBAR-INIT] Saved theme from localStorage:', savedTheme);
+        
+        // Set the theme
+        updateThemeDisplay(savedTheme);
+        
+        // Add click handler - skip in this module since navbar.ts already handles it
+        // This prevents duplicate event listeners
+        console.log('[NAVBAR-INIT] Skipping duplicate theme toggle event listener (handled by navbar.ts)');
+    }
+}
+
+/**
+ * Function to toggle between light and dark themes (DEPRECATED - use themeUtils.toggleTheme instead)
+ */
+function toggleTheme(): void {
+    // Call the imported function instead
+    themeUtils.toggleTheme();
+    return;
+    
+    // DEPRECATED implementation below - kept for reference
+    const body = document.body;
+    const isDarkMode = body.getAttribute('data-theme') === 'dark';
+    
+    if (isDarkMode) {
+        // Switch to light theme
+        body.removeAttribute('data-theme');
+        localStorage.setItem('theme', 'light');
+    } else {
+        // Switch to dark theme
+        body.setAttribute('data-theme', 'dark');
+        localStorage.setItem('theme', 'dark');
+    }
+    
+    // Update the theme icon
+    updateThemeIcon();
+}
+
+/**
+ * Function to update the theme icon based on current theme (DEPRECATED - use themeUtils.updateThemeIcon instead)
+ */
+function updateThemeIcon(): void {
+    // Call the imported function instead
+    themeUtils.updateThemeIcon();
+    return;
+    
+    // DEPRECATED implementation below - kept for reference
+    /*
+    const isDarkMode = document.body.getAttribute('data-theme') === 'dark';
+    const themeIcon = document.getElementById('themeIcon');
+    
+    if (themeIcon) {
+        themeIcon.textContent = isDarkMode ? '☀️' : '🌙';
+        console.log('[NAVBAR-INIT] Updated theme icon to:', themeIcon.textContent);
+    }
+    */
+}
+
+/**
+ * Function to update the theme display based on the saved theme (DEPRECATED - use themeUtils.initializeTheme instead)
+ */
+function updateThemeDisplay(theme: string | null): void {
+    // Set the theme attribute on the body
+    const isDarkMode = theme === 'dark';
+    
+    if (isDarkMode) {
+        document.body.setAttribute('data-theme', 'dark');
+    } else {
+        document.body.removeAttribute('data-theme');
+    }
+    
+    // Update the icon using the shared utility
+    themeUtils.updateThemeIcon();
+}
+
+// Add an immediate execution for the module
+(function() {
+    console.log('[NAVBAR-INIT] Module self-executing function running');
+    
+    // Add a document ready check to ensure the DOM is loaded
+    if (document.readyState === 'loading') {
+        console.log('[NAVBAR-INIT] Document still loading, waiting for DOMContentLoaded');
+        document.addEventListener('DOMContentLoaded', initializeNavbar);
+    } else {
+        // DOM is already loaded, initialize immediately
+        console.log('[NAVBAR-INIT] Document already loaded, initializing immediately');
+        initializeNavbar();
+    }
+
+    // Add direct DOM manipulation as a guaranteed fallback
+    window.addEventListener('load', function() {
+        console.log('[NAVBAR-INIT] Window load event triggered - checking navbar');
+        
+        // Check if navbar has content already
+        const navbarContent = document.querySelector('.nav-left, .nav-right');
+        if (!navbarContent) {
+            console.log('[NAVBAR-INIT] No navbar content found on window load, forcing initialization');
+            initializeNavbar();
+        }
+    });
+    
+    // Set up an additional timeout-based initialization as a fallback
+    // Use a longer timeout to ensure the DOM is definitely ready
+})();
+setTimeout(function() {
+    console.log('[NAVBAR-INIT] Fallback initialization after timeout');
+    
+    // Check if navbar already exists before re-initializing
+    const navbarContent = document.querySelector('.nav-left, .nav-right');
+    if (!navbarContent) {
+        console.log('[NAVBAR-INIT] No navbar content found, performing fallback initialization');
+        initializeNavbar();
+    } else {
+        console.log('[NAVBAR-INIT] Navbar content already exists, skipping fallback initialization');
+    }
+}, 1000);
+
+/**
+ * Sets up the nuclear reset button
+ */
+function setupNuclearReset(): void {
+    console.log('[NAVBAR-INIT] Setting up nuclear reset button');
+    
+    const nuclearResetBtn = document.getElementById('nuclearResetBtn');
+    console.log('[NAVBAR-INIT] Nuclear reset button found:', !!nuclearResetBtn);
+    
+    if (nuclearResetBtn) {
+        // Add click handler
+        nuclearResetBtn.addEventListener('click', function() {
+            console.log('[NAVBAR-INIT] Nuclear reset button clicked');
+            // Confirm before reset
+            if (confirm('This will remove all data and sign you out. Are you sure?')) {
+                resetAllData();
+            }
+        });
+    }
+}
+
+/**
+ * Resets all data and signs out the user
+ */
+function resetAllData(): void {
+    console.log('[NAVBAR-INIT] Executing nuclear reset...');
+    
+    // Clear all localStorage
+    console.log('[NAVBAR-INIT] Clearing localStorage...');
+    localStorage.clear();
+    
+    // Clear sessionStorage
+    console.log('[NAVBAR-INIT] Clearing sessionStorage...');
+    sessionStorage.clear();
+    
+    // Clear any cookies (optional - may not be used in this app)
+    document.cookie.split(';').forEach(cookie => {
+        const eqPos = cookie.indexOf('=');
+        const name = eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim();
+        document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT';
+    });
+    
+    console.log('[NAVBAR-INIT] Data reset complete. Reloading page...');
+    
+    // Reload the page
+    window.location.reload();
+}
+
+// Export functions for potential external use
+export {
+    initializeNavbar,
+    toggleTheme,
+    updateThemeDisplay,
+    resetAllData
+};
\ No newline at end of file
diff --git a/client/src/navbar.ts b/client/src/navbar.ts
new file mode 100644
index 0000000..43b553a
--- /dev/null
+++ b/client/src/navbar.ts
@@ -0,0 +1,233 @@
+/**
+ * navbar.ts
+ * Provides a reusable navigation bar component for all pages
+ */
+
+import { toggleTheme } from './theme-utils';
+
+/**
+ * Navigation items configuration
+ */
+const NAV_ITEMS = [
+    { label: 'HOME', url: './index.html', id: 'nav-home' },
+    { label: 'CLIENT', url: './1120_client.html', id: 'nav-client' },
+    { label: 'SERVER', url: './1120_server.html', id: 'nav-server' },
+    { label: 'BILLBOARD', url: './billboard.html', id: 'nav-billboard' }
+];
+
+/**
+ * Right navigation items (icons)
+ */
+const RIGHT_NAV_ITEMS = [
+    { label: '❓', url: './index.html', title: 'Documentation', id: 'nav-help' },
+    { label: '👤', url: './profile.html', title: 'Profile', id: 'nav-profile' }
+];
+// Flag to track if navbar has been initialized
+let navbarInitialized = false;
+
+/**
+ * Initializes the navigation bar
+ * Updates HTML with appropriate links and sets up event listeners
+ */
+export function initializeNavbar(): void {
+    console.log('%c[Navbar Debug] Initializing navbar component - already initialized: ' + navbarInitialized, 'background: #ff9800; color: white; padding: 2px 5px; border-radius: 3px;');
+    
+    // Only initialize once to prevent conflicts
+    if (navbarInitialized) {
+        console.log('[Navbar Debug] Navbar already initialized, skipping');
+        return;
+    }
+    
+    // Set flag before initializing
+    navbarInitialized = true;
+    
+    // We need to wait for DOM content to be loaded
+    if (document.readyState === 'loading') {
+        console.log('%c[Navbar Debug] Document still loading, waiting for DOMContentLoaded', 'color: #ff9800');
+        document.addEventListener('DOMContentLoaded', () => {
+            console.log('%c[Navbar Debug] DOMContentLoaded event fired, updating navbar now', 'color: #ff9800');
+            updateNavbar();
+        });
+    } else {
+        // DOM is already loaded, update immediately
+        console.log('%c[Navbar Debug] Document already loaded, updating navbar immediately', 'color: #ff9800');
+        updateNavbar();
+    }
+}
+
+/**
+ * Updates the existing navbar in the HTML files
+ */
+function updateNavbar(): void {
+    console.log('%c[Navbar Debug] Updating navbar', 'background: #4CAF50; color: white; padding: 2px 5px; border-radius: 3px;');
+    
+    // Add nav items back to each HTML page
+    // Get the current page URL
+    const currentPageUrl = window.location.pathname;
+    const currentPage = currentPageUrl.split('/').pop() || 'index.html';
+    
+    console.log('[Navbar Debug] Current page:', currentPage);
+    console.log('[Navbar Debug] Current URL:', window.location.href);
+    
+    // Find the navbar container
+    const navbarContainer = document.getElementById('navbarContainer');
+    console.log('[Navbar Debug] Navbar container found:', navbarContainer);
+    
+    // Check if document is fully loaded
+    console.log('[Navbar Debug] Document ready state:', document.readyState);
+    console.log('[Navbar Debug] Body exists:', !!document.body);
+    
+    if (navbarContainer) {
+        // Update the content of the existing navbar container
+        console.log('[Navbar Debug] Updating navbar content');
+        
+        const navbarHtml = `
+        <div class="nav-left">
+            <a href="./index.html" class="nav-link${currentPage === 'index.html' ? ' active' : ''}">HOME</a>
+            <a href="./1120_client.html" class="nav-link${currentPage === '1120_client.html' ? ' active' : ''}">CLIENT</a>
+            <a href="./1120_server.html" class="nav-link${currentPage === '1120_server.html' ? ' active' : ''}">SERVER</a>
+            <a href="./billboard.html" class="nav-link${currentPage === 'billboard.html' ? ' active' : ''}">BILLBOARD</a>
+        </div>
+        <div class="nav-right">
+            <a href="./index.html" class="nav-link nav-icon${currentPage === 'index.html' ? ' active' : ''}" title="Documentation">❓</a>
+            <a href="./profile.html" class="nav-link nav-icon${currentPage === 'profile.html' ? ' active' : ''}" title="Profile">👤</a>
+            <button id="nuclearResetBtn" class="nuclear-reset-btn" title="Reset All Data">💣</button>
+            <button id="themeToggleBtn" class="theme-toggle-btn" title="Toggle Dark Mode">
+                <span id="themeIcon">🌙</span>
+            </button>
+        </div>`;
+        
+        try {
+            // Update the container's HTML
+            console.log('[Navbar Debug] Setting innerHTML:', navbarHtml.substring(0, 50) + '...');
+            navbarContainer.innerHTML = navbarHtml;
+            console.log('[Navbar Debug] Navbar content updated successfully');
+            
+            // Check if the content was properly set
+            console.log('[Navbar Debug] Navbar content after update:', navbarContainer.innerHTML.substring(0, 50) + '...');
+        } catch (error) {
+            console.error('[Navbar Debug] Error updating navbar HTML:', error);
+        }
+    } else {
+        console.error('[Navbar Debug] Navbar container not found');
+    }
+    
+    // Setup theme toggle and nuclear reset button
+    console.log('[Navbar Debug] Setting up theme toggle and nuclear reset button');
+    setupThemeToggle();
+    setupNuclearReset();
+}
+
+/**
+ * Sets up the theme toggle button
+ */
+function setupThemeToggle(): void {
+    console.log('[Navbar Debug] Setting up theme toggle button');
+    
+    // Try various ways to find the theme toggle button
+    const themeToggleBtn = document.getElementById('themeToggleBtn');
+    console.log('[Navbar Debug] Theme toggle button found by ID:', !!themeToggleBtn);
+    
+    // As a fallback, check if there's a button with the theme-toggle-btn class
+    if (!themeToggleBtn) {
+        const toggleBtns = document.getElementsByClassName('theme-toggle-btn');
+        console.log('[Navbar Debug] Theme toggle buttons found by class:', toggleBtns.length);
+    }
+    
+    if (themeToggleBtn) {
+        console.log('[Navbar Debug] Theme toggle button properties:', {
+            tagName: themeToggleBtn.tagName,
+            className: themeToggleBtn.className,
+            innerHTML: themeToggleBtn.innerHTML
+        });
+        
+        // Set initial theme based on local storage
+        const savedTheme = window.localStorage.getItem('theme');
+        console.log('[Navbar Debug] Saved theme from localStorage:', savedTheme);
+        
+        updateThemeDisplay(savedTheme);
+        
+        // Add click handler
+        console.log('[Navbar Debug] Adding click event listener to theme toggle button');
+        themeToggleBtn.addEventListener('click', () => {
+            console.log('[Navbar Debug] Theme toggle button clicked');
+            // Just call the imported toggleTheme function which handles icon updates
+            toggleTheme();
+        });
+        
+        // Verify the event listener was added
+        console.log('[Navbar Debug] Verifying click handler was added - click event attached');
+    } else {
+        console.warn('[Navbar Debug] Theme toggle button not found');
+    }
+}
+
+/**
+ * Sets up the nuclear reset button
+ */
+function setupNuclearReset(): void {
+    console.log('[Navbar Debug] Setting up nuclear reset button');
+    
+    const nuclearResetBtn = document.getElementById('nuclearResetBtn');
+    console.log('[Navbar Debug] Nuclear reset button found:', !!nuclearResetBtn);
+    
+    if (nuclearResetBtn) {
+        console.log('[Navbar Debug] Adding click event listener to nuclear reset button');
+        nuclearResetBtn.addEventListener('click', () => {
+            console.log('[Navbar Debug] Nuclear reset button clicked');
+            
+            // Confirm before reset
+            if (confirm('This will remove all data and sign you out. Are you sure?')) {
+                resetAllData();
+            }
+        });
+    } else {
+        console.warn('[Navbar Debug] Nuclear reset button not found');
+    }
+}
+
+/**
+ * Resets all data and signs out the user
+ */
+export function resetAllData(): void {
+    console.log('[Navbar Debug] Executing nuclear reset...');
+    
+    // Clear all localStorage
+    console.log('[Navbar Debug] Clearing localStorage...');
+    localStorage.clear();
+    
+    // Clear sessionStorage
+    console.log('[Navbar Debug] Clearing sessionStorage...');
+    sessionStorage.clear();
+    
+    // Clear any cookies (optional - may not be used in this app)
+    document.cookie.split(';').forEach(cookie => {
+        const eqPos = cookie.indexOf('=');
+        const name = eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim();
+        document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 GMT';
+    });
+    
+    console.log('[Navbar Debug] Data reset complete. Reloading page...');
+    
+    // Reload the page
+    window.location.reload();
+}
+
+/**
+ * Updates the theme icon based on the current theme
+ * @param theme The current theme ('dark' or 'light')
+ */
+function updateThemeDisplay(theme: string | null): void {
+    const isDarkMode = theme === 'dark';
+    const themeIcon = document.getElementById('themeIcon');
+    
+    if (isDarkMode) {
+        document.body.setAttribute('data-theme', 'dark');
+    } else {
+        document.body.removeAttribute('data-theme');
+    }
+    
+    if (themeIcon) {
+        themeIcon.textContent = isDarkMode ? '☀️' : '🌙';
+    }
+}
\ No newline at end of file
diff --git a/client/src/receiver.ts b/client/src/receiver.ts
index 7d3c806..d52f10c 100644
--- a/client/src/receiver.ts
+++ b/client/src/receiver.ts
@@ -8,6 +8,8 @@ import * as nostrTools from 'nostr-tools';
 import { defaultServerConfig } from './config';
 import { HttpService } from './services/HttpService';
 import { NostrService } from './services/NostrService';
+import { ReceivedEvent } from './services/NostrEventService';
+import { decryptKeyWithNostrExtension, decryptKeyWithNostrTools, decryptWithWebCrypto } from './utils/crypto-utils';
 import { UiService } from './services/UiService';
 
 // Module-level service instances
@@ -93,6 +95,75 @@ async function handleConnectRelay(): Promise<void> {
       const filter = nostrService.createKind21120Filter(showAllEvents);
       await nostrService.subscribeToEvents(filter);
       
+      // Set event handler to process received events
+      nostrService.setEventHandler(async (event) => {
+        // Process 21120 (requests) and 21121 (responses) events
+        if ((event.kind === 21120 || event.kind === 21121) && event.id) {
+          // Initialize ReceivedEvent object with potential decrypted content
+          const receivedEvent = {
+            id: event.id,
+            event: event,
+            receivedAt: Date.now(),
+            decrypted: false,
+            decryptedContent: undefined as string | undefined
+          };
+          
+          // Try to decrypt the content if it's a 21120 event
+          if (event.kind === 21120) {
+            try {
+              // Find the key tag which contains the encryption key
+              const keyTag = event.tags.find(tag => tag[0] === 'key');
+              if (keyTag && keyTag.length > 1 && keyTag[1]) {
+                const encryptedKey = keyTag[1];
+                
+                // Get the server private key from localStorage
+                const serverNsec = localStorage.getItem('serverNsec');
+                if (serverNsec) {
+                  try {
+                    // We'll use the server's nsec directly with nostr-tools library
+                    console.log("Attempting to decrypt key with nostr-tools using server nsec", {
+                      encryptedKeyLength: encryptedKey.length,
+                      serverNsecPrefix: serverNsec.substring(0, 5) + '...'
+                    });
+                    
+                    // Extract the client's pubkey (sender)
+                    const clientPubkey = event.pubkey;
+                    console.log("Using client pubkey for decryption:", clientPubkey.substring(0, 8) + "...");
+                    
+                    // Use the decryptKeyWithNostrTools function which uses the library directly
+                    const decryptionKey = await decryptKeyWithNostrTools(encryptedKey, serverNsec, clientPubkey);
+                    console.log("Successfully decrypted key:", decryptionKey.substring(0, 5) + '...');
+                    
+                    // Decrypt the content using WebCrypto
+                    console.log("Attempting to decrypt content with WebCrypto");
+                    const decryptedContent = await decryptWithWebCrypto(event.content, decryptionKey);
+                    
+                    // Update the receivedEvent with decrypted content
+                    receivedEvent.decrypted = true;
+                    receivedEvent.decryptedContent = decryptedContent;
+                    console.log("Successfully decrypted 21120 event content");
+                  } catch (error) {
+                    console.error("Failed to decrypt event content:", error);
+                    console.error("Decryption error details:", {
+                      errorName: error instanceof Error ? error.name : 'Unknown',
+                      errorMessage: error instanceof Error ? error.message : String(error),
+                      eventId: event.id.substring(0, 8) + '...',
+                      hasKeyTag: !!keyTag,
+                      hasServerNsec: !!serverNsec
+                    });
+                  }
+                }
+              }
+            } catch (error) {
+              console.error("Error attempting to decrypt event:", error);
+            }
+          }
+          
+          // Add to UI via UiService (with decrypted content if successful)
+          uiService.addReceivedEvent(receivedEvent);
+        }
+      });
+      
       uiService.updateRelayStatus('Connected and subscribed ✓', 'connected');
     } catch (error) {
       console.error("Subscription error:", error);
@@ -199,97 +270,72 @@ async function initializeServerNpub(): Promise<void> {
           
           // Query for existing 31120 event
           try {
-            console.log("Querying relay for 31120 event...");
-            // Pass the user's pubkey to look for 31120 events published by this user
-            const userPubkey = nostrService.getLoggedInPubkey();
-            const existingEvent = await nostrService.queryFor31120Event(relayUrl, userPubkey);
-            
-            if (existingEvent) {
-              console.log("Found existing 31120 event:", existingEvent);
-              
-              // Look for the d tag to find the server pubkey
-              const dTag = existingEvent.tags.find(tag => tag[0] === 'd');
-              if (dTag && dTag.length > 1) {
-                // Display the server pubkey
-                // Store both formats
-                serverPubkeyHex = dTag[1];
-                try {
-                  serverPubkeyNpub = nostrTools.nip19.npubEncode(dTag[1]);
+            // Check first if we have a serverNsec in localStorage
+            const serverNsec = localStorage.getItem('serverNsec');
+            if (serverNsec) {
+              try {
+                // Extract the server pubkey from the saved nsec
+                const decoded = nostrTools.nip19.decode(serverNsec);
+                if (decoded.type === 'nsec') {
+                  const privateKeyBytes = decoded.data as any;
+                  serverPubkeyHex = nostrTools.getPublicKey(privateKeyBytes);
+                  serverPubkeyNpub = nostrTools.nip19.npubEncode(serverPubkeyHex);
+                  
+                  console.log(`Using server key from localStorage: ${serverPubkeyHex.substring(0, 8)}...`);
+                  
+                  // Set the server pubkey in the input field
                   serverNpubInput.value = serverPubkeyNpub;
-                  console.log("Using server pubkey from existing 31120 event:", serverPubkeyNpub);
-                } catch (error) {
-                  console.error("Error encoding server pubkey:", error);
-                  serverNpubInput.value = dTag[1];
-                  serverPubkeyNpub = dTag[1]; // Fallback to hex if conversion fails
-                }
-              }
-              
-              // Display the raw JSON for debugging
-              console.log("31120 Event JSON:", JSON.stringify(existingEvent, null, 2));
-              
-              if (relayStatusElement) {
-                relayStatusElement.textContent = "Server registration found ✓";
-                relayStatusElement.className = "relay-status connected";
-              }
-            } else {
-              console.log("No 31120 event found, creating one...");
-              
-              if (relayStatusElement) {
-                relayStatusElement.textContent = "Creating server registration...";
-                relayStatusElement.className = "relay-status connecting";
-              }
-              
-              // Create a new 31120 event
-              const newEvent = await nostrService.create31120Event(relayUrl);
-              
-              if (newEvent) {
-                console.log("Created new 31120 event:", newEvent);
-                
-                // Look for the d tag to find the server pubkey
-                const dTag = newEvent.tags.find(tag => tag[0] === 'd');
-                if (dTag && dTag.length > 1) {
-                  // Display the server pubkey
-                  // Store both formats
-                  serverPubkeyHex = dTag[1];
-                  try {
-                    serverPubkeyNpub = nostrTools.nip19.npubEncode(dTag[1]);
-                    serverNpubInput.value = serverPubkeyNpub;
-                    console.log("Using server pubkey from new 31120 event:", serverPubkeyNpub);
-                  } catch (error) {
-                    console.error("Error encoding server pubkey:", error);
-                    serverNpubInput.value = dTag[1];
-                    serverPubkeyNpub = dTag[1]; // Fallback to hex if conversion fails
+                  
+                  if (relayStatusElement) {
+                    relayStatusElement.textContent = "Server key loaded from localStorage ✓";
+                    relayStatusElement.className = "relay-status connected";
                   }
                 }
+              } catch (error) {
+                console.error('Error decoding server nsec:', error);
                 
-                // Display the raw JSON for debugging
-                console.log("31120 Event JSON:", JSON.stringify(newEvent, null, 2));
-                
-                // Create a debug output area to show the JSON
-                const eventDebugOutput = document.createElement('div');
-                eventDebugOutput.className = 'event-debug-output';
-                eventDebugOutput.innerHTML = `
-                  <h3>31120 Event JSON (Debug)</h3>
-                  <pre>${JSON.stringify(newEvent, null, 2)}</pre>
+                // Error reading the server key
+                const keyErrorMessage = document.createElement('div');
+                keyErrorMessage.className = 'info-notice';
+                keyErrorMessage.innerHTML = `
+                  <p>There was an error reading your server key.</p>
+                  <p>Visit the <a href="billboard.html">Billboard</a> page to generate a new server key.</p>
                 `;
                 
-                // Insert it after the server info container
+                // Insert message after the server info container
                 const serverInfoContainer = document.querySelector('.server-info-container');
                 if (serverInfoContainer) {
-                  serverInfoContainer.parentNode?.insertBefore(eventDebugOutput, serverInfoContainer.nextSibling);
+                  serverInfoContainer.parentNode?.insertBefore(keyErrorMessage, serverInfoContainer.nextSibling);
                 }
                 
-                if (relayStatusElement) {
-                  relayStatusElement.textContent = "Server registration created ✓";
-                  relayStatusElement.className = "relay-status connected";
-                }
-              } else {
-                console.error("Failed to create 31120 event");
-                if (relayStatusElement) {
-                  relayStatusElement.textContent = "Failed to create server registration";
-                  relayStatusElement.className = "relay-status error";
-                }
+                // Set a placeholder in the server pubkey field
+                serverNpubInput.value = "Error reading server key";
               }
+            } else {
+              console.log("No server key found in localStorage");
+              
+              if (relayStatusElement) {
+                relayStatusElement.textContent = "No server key found";
+                relayStatusElement.className = "relay-status error";
+              }
+              
+              // Display message about needing a server key
+              const keyNeededMessage = document.createElement('div');
+              keyNeededMessage.className = 'info-notice';
+              keyNeededMessage.innerHTML = `
+                <p>No server key found. This server needs a key to decrypt and sign responses to HTTP-over-Nostr messages.</p>
+                <p>Visit the <a href="billboard.html">Billboard</a> page to generate a server key.</p>
+              `;
+              
+              // Insert message after the server info container
+              const serverInfoContainer = document.querySelector('.server-info-container');
+              if (serverInfoContainer) {
+                serverInfoContainer.parentNode?.insertBefore(keyNeededMessage, serverInfoContainer.nextSibling);
+              }
+              
+              // Set a placeholder in the server pubkey field
+              serverNpubInput.value = "No server key - visit Billboard page";
+              // No need to check for serverNsec again, as we've already handled that case above
             }
           } catch (eventError) {
             console.error("Error handling 31120 event:", eventError);
@@ -317,16 +363,83 @@ async function initializeServerNpub(): Promise<void> {
     toggleFormatBtn.addEventListener('click', () => {
       if (currentFormat === 'npub') {
         // Switch to hex format
-        serverNpubInput.value = serverPubkeyHex;
-        currentFormat = 'hex';
-        formatIndicator.textContent = 'Currently showing: HEX format';
-        document.getElementById('formatBtnText')!.textContent = 'NPUB';
+        // Get the current value from the input field
+        const currentValue = serverNpubInput.value.trim();
+        
+        // Check if we have a valid hex value, if not, try to derive it from the current value
+        if (!serverPubkeyHex || serverPubkeyHex === serverPubkeyNpub) {
+          try {
+            // If current value is an npub, decode it to get the hex value
+            if (currentValue.startsWith('npub')) {
+              const decoded = nostrTools.nip19.decode(currentValue);
+              if (decoded.type === 'npub') {
+                serverPubkeyHex = decoded.data as string;
+                console.log("Converted npub to hex:", serverPubkeyHex);
+              }
+            } else if (/^[0-9a-f]{64}$/i.test(currentValue)) {
+              // If it's already a valid hex format
+              serverPubkeyHex = currentValue;
+            } else {
+              // Try as a last resort with the stored npub value
+              if (serverPubkeyNpub && serverPubkeyNpub.startsWith('npub')) {
+                const decoded = nostrTools.nip19.decode(serverPubkeyNpub);
+                if (decoded.type === 'npub') {
+                  serverPubkeyHex = decoded.data as string;
+                }
+              }
+            }
+          } catch (error) {
+            console.error("Error converting to hex:", error);
+            // Don't update if we can't get a valid hex value
+            return;
+            serverPubkeyHex = serverPubkeyNpub;
+          }
+        }
+        
+        // Only switch if we have a value to display
+        if (serverPubkeyHex) {
+          serverNpubInput.value = serverPubkeyHex;
+          currentFormat = 'hex';
+          formatIndicator.textContent = 'Currently showing: HEX format';
+          document.getElementById('formatBtnText')!.textContent = 'NPUB';
+        }
       } else {
         // Switch to npub format
-        serverNpubInput.value = serverPubkeyNpub;
-        currentFormat = 'npub';
-        formatIndicator.textContent = 'Currently showing: NPUB format';
-        document.getElementById('formatBtnText')!.textContent = 'HEX';
+        // Check if we have a valid npub value, if not, try to derive it from hex
+        // Get the current value from the input field
+        const currentValue = serverNpubInput.value.trim();
+        
+        // Check if we have a valid npub value, if not, try to derive it from the current value
+        if (!serverPubkeyNpub || serverPubkeyNpub === serverPubkeyHex) {
+          try {
+            // If current value is a hex key, encode it to get the npub
+            if (/^[0-9a-f]{64}$/i.test(currentValue)) {
+              serverPubkeyNpub = nostrTools.nip19.npubEncode(currentValue);
+              console.log("Converted hex to npub:", serverPubkeyNpub);
+            } else if (currentValue.startsWith('npub')) {
+              // If it's already a valid npub format
+              serverPubkeyNpub = currentValue;
+            } else {
+              // Try as a last resort with the stored hex value
+              if (serverPubkeyHex && /^[0-9a-f]{64}$/i.test(serverPubkeyHex)) {
+                serverPubkeyNpub = nostrTools.nip19.npubEncode(serverPubkeyHex);
+              }
+            }
+          } catch (error) {
+            console.error("Error converting to npub:", error);
+            // Don't update if we can't get a valid npub value
+            return;
+          }
+        }
+        
+        // Only switch if we have a value to display
+        if (serverPubkeyNpub) {
+          serverNpubInput.value = serverPubkeyNpub;
+          currentFormat = 'npub';
+          formatIndicator.textContent = 'Currently showing: NPUB format';
+          
+          document.getElementById('formatBtnText')!.textContent = 'HEX';
+        }
       }
     });
   }
@@ -388,6 +501,75 @@ async function autoConnectToDefaultRelay(): Promise<void> {
             const filter = nostrService.createKind21120Filter(showAllEvents);
             await nostrService.subscribeToEvents(filter);
             
+            // Set event handler to process received events
+            nostrService.setEventHandler(async (event) => {
+              // Process 21120 (requests) and 21121 (responses) events
+              if ((event.kind === 21120 || event.kind === 21121) && event.id) {
+                // Initialize ReceivedEvent object with potential decrypted content
+                const receivedEvent = {
+                  id: event.id,
+                  event: event,
+                  receivedAt: Date.now(),
+                  decrypted: false,
+                  decryptedContent: undefined as string | undefined
+                };
+                
+                // Try to decrypt the content if it's a 21120 event
+                if (event.kind === 21120) {
+                  try {
+                    // Find the key tag which contains the encryption key
+                    const keyTag = event.tags.find(tag => tag[0] === 'key');
+                    if (keyTag && keyTag.length > 1 && keyTag[1]) {
+                      const encryptedKey = keyTag[1];
+                      
+                      // Get the server private key from localStorage
+                      const serverNsec = localStorage.getItem('serverNsec');
+                      if (serverNsec) {
+                        try {
+                          // We'll use the server's nsec directly with nostr-tools library
+                          console.log("Attempting to decrypt key with nostr-tools using server nsec", {
+                            encryptedKeyLength: encryptedKey.length,
+                            serverNsecPrefix: serverNsec.substring(0, 5) + '...'
+                          });
+                          
+                          // Extract the client's pubkey (sender)
+                          const clientPubkey = event.pubkey;
+                          console.log("Using client pubkey for decryption:", clientPubkey.substring(0, 8) + "...");
+                          
+                          // Use the decryptKeyWithNostrTools function which uses the library directly
+                          const decryptionKey = await decryptKeyWithNostrTools(encryptedKey, serverNsec, clientPubkey);
+                          console.log("Successfully decrypted key:", decryptionKey.substring(0, 5) + '...');
+                          
+                          // Decrypt the content using WebCrypto
+                          console.log("Attempting to decrypt content with WebCrypto");
+                          const decryptedContent = await decryptWithWebCrypto(event.content, decryptionKey);
+                          
+                          // Update the receivedEvent with decrypted content
+                          receivedEvent.decrypted = true;
+                          receivedEvent.decryptedContent = decryptedContent;
+                          console.log("Successfully decrypted 21120 event content");
+                        } catch (error) {
+                          console.error("Failed to decrypt event content:", error);
+                          console.error("Decryption error details:", {
+                            errorName: error instanceof Error ? error.name : 'Unknown',
+                            errorMessage: error instanceof Error ? error.message : String(error),
+                            eventId: event.id.substring(0, 8) + '...',
+                            hasKeyTag: !!keyTag,
+                            hasServerNsec: !!serverNsec
+                          });
+                        }
+                      }
+                    }
+                  } catch (error) {
+                    console.error("Error attempting to decrypt event:", error);
+                  }
+                }
+                
+                // Add to UI via UiService (with decrypted content if successful)
+                uiService.addReceivedEvent(receivedEvent);
+              }
+            });
+            
             uiService.updateRelayStatus('Connected and subscribed ✓', 'connected');
           } catch (error) {
             uiService.updateRelayStatus(
diff --git a/client/src/services/EventCache.ts b/client/src/services/EventCache.ts
new file mode 100644
index 0000000..eb7b970
--- /dev/null
+++ b/client/src/services/EventCache.ts
@@ -0,0 +1,125 @@
+/**
+ * EventCache.ts
+ * Handles caching of Nostr events to reduce network requests
+ */
+
+import type { NostrEvent } from '../relay';
+
+import { getServerPubkeyFromEvent } from './NostrUtils';
+
+export interface CacheEntry {
+  timestamp: number;
+  events: {[key: string]: NostrEvent};
+}
+
+export class EventCache {
+  private memoryCache = new Map<string, CacheEntry>();
+  private cachePrefix: string;
+  private cacheExpiry: number;
+  
+  /**
+   * Create a new event cache instance
+   * @param prefix Storage prefix for localStorage keys
+   * @param expiryMs Cache expiry time in milliseconds
+   */
+  constructor(prefix: string = 'nostr_cache_', expiryMs: number = 5 * 60 * 1000) {
+    this.cachePrefix = prefix;
+    this.cacheExpiry = expiryMs;
+  }
+  
+  /**
+   * Store events in the cache
+   * @param key Cache key (usually relay URL)
+   * @param events Events to cache
+   */
+  public cacheEvents(key: string, events: NostrEvent[]): void {
+    // Create a map of events by server pubkey
+    const eventsMap: {[key: string]: NostrEvent} = {};
+    
+    for (const event of events) {
+      const serverPubkey = getServerPubkeyFromEvent(event);
+      if (serverPubkey) {
+        eventsMap[serverPubkey] = event;
+      }
+    }
+    
+    const timestamp = Date.now();
+    const cacheData: CacheEntry = {
+      timestamp,
+      events: eventsMap
+    };
+    
+    // Store in memory cache
+    this.memoryCache.set(key, cacheData);
+    
+    // Store in localStorage for persistence
+    try {
+      localStorage.setItem(this.cachePrefix + key, JSON.stringify(cacheData));
+      console.log(`Cached ${Object.keys(eventsMap).length} events for ${key}`);
+    } catch (error) {
+      console.error('Failed to store cache in localStorage:', error);
+      // Keep the memory cache even if localStorage fails
+    }
+  }
+  
+  /**
+   * Get cached events
+   * @param key Cache key (usually relay URL)
+   * @returns Array of cached events or null if cache is invalid/expired
+   */
+  public getEvents(key: string): NostrEvent[] | null {
+    const now = Date.now();
+    
+    // Try memory cache first (fastest)
+    const memoryEntry = this.memoryCache.get(key);
+    if (memoryEntry && now - memoryEntry.timestamp < this.cacheExpiry) {
+      return Object.values(memoryEntry.events);
+    }
+    
+    // Try localStorage (persists between page navigations)
+    try {
+      const storedData = localStorage.getItem(this.cachePrefix + key);
+      if (storedData) {
+        const parsedData = JSON.parse(storedData) as CacheEntry;
+        
+        // Check if cache is still valid
+        if (now - parsedData.timestamp < this.cacheExpiry) {
+          // Update memory cache
+          this.memoryCache.set(key, parsedData);
+          return Object.values(parsedData.events);
+        }
+      }
+    } catch (error) {
+      console.error('Error reading cache from localStorage:', error);
+    }
+    
+    // Cache miss or expired
+    return null;
+  }
+  
+  /**
+   * Clear cache for a specific key or all cache entries
+   * @param key Optional cache key (if not provided, clears all cache)
+   */
+  public clearCache(key?: string): void {
+    if (key) {
+      // Clear specific cache entry
+      this.memoryCache.delete(key);
+      localStorage.removeItem(this.cachePrefix + key);
+      console.log(`Cleared cache for ${key}`);
+    } else {
+      // Clear all cache entries
+      this.memoryCache.clear();
+      
+      // Clear all localStorage entries with our prefix
+      for (let i = 0; i < localStorage.length; i++) {
+        const storageKey = localStorage.key(i);
+        if (storageKey && storageKey.startsWith(this.cachePrefix)) {
+          localStorage.removeItem(storageKey);
+        }
+      }
+      
+      console.log('Cleared all cached events');
+    }
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/EventDetailsRenderer.ts b/client/src/services/EventDetailsRenderer.ts
new file mode 100644
index 0000000..8cd6739
--- /dev/null
+++ b/client/src/services/EventDetailsRenderer.ts
@@ -0,0 +1,196 @@
+/**
+ * EventDetailsRenderer.ts
+ * Component for rendering detailed event information
+ */
+
+import { NostrEvent } from '../relay';
+import { ReceivedEvent } from './NostrEventService';
+import { HttpFormatter } from './HttpFormatter';
+
+/**
+ * Class for rendering event details in the UI
+ */
+export class EventDetailsRenderer {
+  private eventDetails: HTMLElement | null = null;
+  private receivedEvents: Map<string, ReceivedEvent>;
+  private relatedEvents: Map<string, string[]>;
+  
+  /**
+   * Constructor
+   * @param receivedEvents Map of received events
+   * @param relatedEvents Map of related events
+   */
+  constructor(receivedEvents: Map<string, ReceivedEvent>, relatedEvents: Map<string, string[]>) {
+    this.receivedEvents = receivedEvents;
+    this.relatedEvents = relatedEvents;
+  }
+  
+  /**
+   * Initialize the event details element
+   */
+  public initialize(): void {
+    this.eventDetails = document.getElementById('eventDetails');
+  }
+  
+  /**
+   * Show event details for a given event ID
+   * @param eventId The event ID to display details for
+   */
+  public showEventDetails(eventId: string): void {
+    if (!this.eventDetails || !eventId) {
+      return;
+    }
+    
+    // Get the received event from our map
+    const receivedEvent = this.receivedEvents.get(eventId);
+    if (!receivedEvent || !receivedEvent.event) {
+      this.eventDetails.innerHTML = '<div class="no-event">Event not found</div>';
+      return;
+    }
+    
+    // Get the event
+    const event = receivedEvent.event;
+    
+    // Determine if it's a request or response
+    const isRequest = event.kind === 21120;
+    const isResponse = event.kind === 21121;
+    const is21121Event = event.kind === 21121;
+    
+    // Determine the content to display
+    let httpContent = receivedEvent.decrypted ? 
+      receivedEvent.decryptedContent || event.content : 
+      event.content;
+    
+    // Find related events (responses for requests, or request for responses)
+    let relatedEventsHtml = '';
+    let relatedIds: string[] = [];
+    
+    if (event.id) {
+      // Get related events from the map
+      relatedIds = this.relatedEvents.get(event.id) || [];
+      
+      if (relatedIds.length > 0) {
+        relatedEventsHtml = `
+          <div class="related-events">
+            <h3>Related ${isRequest ? 'Responses' : 'Request'}</h3>
+            <ul class="related-events-list">
+              ${relatedIds.map(id => {
+                const relatedEvent = this.receivedEvents.get(id)?.event;
+                const relatedType = relatedEvent?.kind === 21120 ? 'Request' : 'Response';
+                return `
+                  <li>
+                    <a href="#" class="related-event-link" data-id="${id}">
+                      ${relatedType} (${id.substring(0, 8)}...)
+                    </a>
+                  </li>
+                `;
+              }).join('')}
+            </ul>
+          </div>
+        `;
+      }
+    }
+    
+    // Format based on event type
+    const eventTime = new Date(event.created_at * 1000).toLocaleString();
+    
+    // Handle for "Execute HTTP Request" button for request events
+    const execRequestBtn = isRequest ? `<button class="execute-http-request-btn">Execute HTTP Request</button>` : '';
+    
+    // Create response button for requests that don't have responses yet
+    const createResponseBtn = (isRequest && relatedIds.length === 0) ? 
+      `<button class="create-response-btn">Create NIP-21121 Response</button>` : '';
+    
+    this.eventDetails.innerHTML = `
+      <div class="event-details-header">
+        <h2>Event Details</h2>
+        <span class="event-id-display">ID: ${event.id?.substring(0, 8) || 'Unknown'}...</span>
+      </div>
+      
+      <div class="event-type-info">
+        <span class="event-kind">Kind: ${event.kind}</span>
+        <span class="event-type">${isRequest ? 'HTTP Request' : (isResponse ? 'HTTP Response' : 'Unknown')}</span>
+        <span class="event-time">Time: ${eventTime}</span>
+      </div>
+      
+      <div class="event-metadata">
+        <div class="pubkey">Pubkey: ${event.pubkey}</div>
+        <div class="tags">
+          <h3>Tags</h3>
+          <pre>${JSON.stringify(event.tags, null, 2)}</pre>
+        </div>
+      </div>
+      
+      ${relatedEventsHtml}
+      
+      <div class="http-actions">
+        ${execRequestBtn}
+        ${createResponseBtn}
+      </div>
+      
+      <div class="http-content-tabs">
+        <div class="tab-buttons">
+          <button class="tab-btn" data-tab="raw-http">Raw HTTP</button>
+          <button class="tab-btn active" data-tab="formatted-http">Formatted HTTP</button>
+        </div>
+        
+        <div class="tab-content" id="raw-http">
+          ${isRequest ?
+            `<div class="http-content-header">
+              <button class="execute-http-request-btn">Execute HTTP Request</button>
+            </div>` :
+            ''
+          }
+          <pre class="http-content">${httpContent}</pre>
+          ${!receivedEvent.decrypted ?
+            '<div class="decryption-status error" id="decryption-status-' + eventId + '">Decryption failed - NIP-44 implementation may be incompatible or missing. Check console for errors.</div>' :
+            '<div class="decryption-status success" id="decryption-status-' + eventId + '">Decryption successful ✓</div>'}
+        </div>
+        <div class="tab-content active" id="formatted-http">
+          ${isRequest ?
+            `<div class="http-content-header">
+              <button class="execute-http-request-btn">Execute HTTP Request</button>
+            </div>` :
+            ''
+          }
+          <div class="http-formatted-container">
+            ${HttpFormatter.formatHttpContent(httpContent, isRequest, isResponse || is21121Event)}
+          </div>
+          ${!receivedEvent.decrypted ?
+            '<div class="decryption-status error" id="decryption-status-' + eventId + '">Decryption failed - NIP-44 implementation may be incompatible or missing. Check console for errors.</div>' :
+            '<div class="decryption-status success" id="decryption-status-' + eventId + '">Decryption successful ✓</div>'}
+        </div>
+      </div>
+    `;
+    
+    // Set up tab buttons
+    const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
+    tabButtons.forEach(button => {
+      button.addEventListener('click', (e) => {
+        // Remove active class from all buttons and content
+        tabButtons.forEach(btn => btn.classList.remove('active'));
+        
+        const tabContents = this.eventDetails!.querySelectorAll('.tab-content');
+        tabContents.forEach(content => content.classList.remove('active'));
+        
+        // Add active class to clicked button
+        button.classList.add('active');
+        
+        // Show corresponding content
+        const tabId = (button as HTMLElement).dataset.tab || '';
+        const tabContent = this.eventDetails!.querySelector(`#${tabId}`);
+        if (tabContent) {
+          tabContent.classList.add('active');
+        }
+      });
+    });
+  }
+  
+  /**
+   * Get the event details element
+   * @returns The event details element or null
+   */
+  public getEventDetailsElement(): HTMLElement | null {
+    return this.eventDetails;
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/EventListRenderer.ts b/client/src/services/EventListRenderer.ts
new file mode 100644
index 0000000..a186db2
--- /dev/null
+++ b/client/src/services/EventListRenderer.ts
@@ -0,0 +1,139 @@
+/**
+ * EventListRenderer.ts
+ * Component for rendering events in the UI list
+ */
+
+import * as nostrTools from 'nostr-tools';
+import { NostrEvent } from '../relay';
+
+/**
+ * Class for rendering events in the UI list
+ */
+export class EventListRenderer {
+  private eventsList: HTMLElement | null = null;
+  
+  /**
+   * Initialize the event list element
+   */
+  public initialize(): void {
+    this.eventsList = document.getElementById('eventsList');
+  }
+  
+  /**
+   * Add an event to the list
+   * @param event The Nostr event to add
+   * @param kind The event kind (21120 or 21121)
+   */
+  public addEventToList(event: NostrEvent, kind: number): HTMLElement | null {
+    if (!this.eventsList || !event) {
+      return null;
+    }
+    
+    // Create a container for the event
+    const eventItem = document.createElement('div');
+    eventItem.className = 'event-item';
+    eventItem.dataset.id = event.id || '';
+    
+    // Format event ID for display
+    const eventIdForDisplay = event.id ? event.id.substring(0, 8) : 'Unknown';
+    
+    // Set event type
+    let eventType = 'Unknown';
+    if (kind === 21120) {
+      eventType = 'HTTP Request';
+    } else if (kind === 21121) {
+      eventType = 'HTTP Response';
+    }
+    
+    // Find recipient if any
+    let recipient = '';
+    let isToServer = false;
+    
+    // Check for p tag to identify recipient
+    const pTag = event.tags.find(tag => tag[0] === 'p');
+    if (pTag && pTag.length > 1) {
+      recipient = `<div class="recipient">To: ${pTag[1].substring(0, 8)}...</div>`;
+      
+      // Check if this message is addressed to our server
+      try {
+        // Get the server pubkey from localStorage
+        const serverNsec = localStorage.getItem('serverNsec');
+        if (serverNsec) {
+          const decoded = nostrTools.nip19.decode(serverNsec);
+          if (decoded.type === 'nsec') {
+            // Get server public key
+            const serverPubkey = nostrTools.getPublicKey(decoded.data as any);
+            
+            // Check if the p tag matches our server pubkey
+            isToServer = (pTag[1] === serverPubkey);
+          }
+        }
+      } catch {
+        // Ignore errors when checking
+      }
+    }
+    
+    // Format the event item
+    eventItem.innerHTML = `
+      <div class="event-item-container">
+        <div class="event-avatar" data-pubkey="${event.pubkey}">
+          <div class="avatar-placeholder">👤</div>
+        </div>
+        <div class="event-content-wrapper">
+          <div class="event-header">
+            <div class="event-type ${eventType === 'HTTP Request' ? 'request' : 'response'}">${eventType}</div>
+            <div class="event-time">${new Date(event.created_at * 1000).toLocaleTimeString()}</div>
+          </div>
+          <div class="event-id">ID: ${eventIdForDisplay}... ${recipient} ${isToServer ? '<span class="server-match">✓</span>' : ''}</div>
+          <div class="event-pubkey">From: ${event.pubkey.substring(0, 8)}...</div>
+        </div>
+      </div>
+    `;
+    
+    // Set a data attribute to indicate if this event is addressed to our server
+    eventItem.dataset.toServer = isToServer.toString();
+    
+    // Add to list at the top
+    if (this.eventsList.firstChild) {
+      this.eventsList.insertBefore(eventItem, this.eventsList.firstChild);
+    } else {
+      this.eventsList.appendChild(eventItem);
+    }
+    
+    return eventItem;
+  }
+  
+  /**
+   * Filter events in the UI based on the showAllEvents checkbox state
+   * @param showAllEvents Whether to show all events or only those for the server
+   */
+  public filterEventsInUI(showAllEvents: boolean): void {
+    if (!this.eventsList) {
+      return;
+    }
+    
+    // Get all event items
+    const eventItems = this.eventsList.querySelectorAll('.event-item');
+    
+    // Iterate through each event item
+    eventItems.forEach((item) => {
+      const isToServer = item.getAttribute('data-to-server') === 'true';
+      
+      if (showAllEvents) {
+        // Show all events
+        (item as HTMLElement).style.display = '';
+      } else {
+        // Only show events addressed to the server
+        (item as HTMLElement).style.display = isToServer ? '' : 'none';
+      }
+    });
+  }
+  
+  /**
+   * Get the events list element
+   * @returns The events list element or null
+   */
+  public getEventsList(): HTMLElement | null {
+    return this.eventsList;
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/HttpClient.ts b/client/src/services/HttpClient.ts
new file mode 100644
index 0000000..46ebdbe
--- /dev/null
+++ b/client/src/services/HttpClient.ts
@@ -0,0 +1,65 @@
+/**
+ * HttpClient.ts
+ * Service for making HTTP requests
+ */
+
+import { HttpService } from './HttpService';
+import { ToastNotifier } from './ToastNotifier';
+
+/**
+ * Class for handling HTTP requests
+ */
+export class HttpClient {
+  private httpService: HttpService;
+  
+  /**
+   * Constructor
+   * @param httpService An instance of HttpService
+   */
+  constructor(httpService: HttpService) {
+    this.httpService = httpService;
+  }
+  
+  /**
+   * Send an HTTP request based on raw content
+   * @param httpContent The raw HTTP content (request)
+   * @returns Promise resolving to the HTTP response
+   */
+  public async sendHttpRequest(httpContent: string): Promise<string> {
+    try {
+      // Parse the HTTP request
+      const parsedRequest = this.httpService.parseHttpRequest(httpContent);
+      
+      if (!parsedRequest) {
+        throw new Error('Failed to parse HTTP request');
+      }
+      
+      const { url, options } = parsedRequest;
+      const { method, headers, body } = options;
+      
+      // Perform fetch
+      const response = await fetch(url, {
+        method,
+        headers,
+        body: body ? body.trim() : undefined,
+      });
+      
+      // Build response string
+      let responseText = `HTTP/1.1 ${response.status} ${response.statusText}\n`;
+      
+      // Add headers
+      response.headers.forEach((value, key) => {
+        responseText += `${key}: ${value}\n`;
+      });
+      
+      // Add body
+      const responseBody = await response.text();
+      responseText += '\n' + responseBody;
+      
+      return responseText;
+    } catch (error) {
+      console.error('Error sending HTTP request:', error);
+      return `HTTP/1.1 500 Internal Server Error\nContent-Type: text/plain\n\nError: ${error instanceof Error ? error.message : String(error)}`;
+    }
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/HttpFormatter.ts b/client/src/services/HttpFormatter.ts
new file mode 100644
index 0000000..11facdf
--- /dev/null
+++ b/client/src/services/HttpFormatter.ts
@@ -0,0 +1,149 @@
+/**
+ * HttpFormatter.ts
+ * Utility for formatting HTTP content
+ */
+
+/**
+ * Class for formatting HTTP content
+ */
+export class HttpFormatter {
+  /**
+   * Format HTTP content for display
+   * @param content The raw HTTP content
+   * @param isRequest Whether this is a request or response
+   * @param isResponse Whether this is a response or other content
+   * @returns Formatted HTML
+   */
+  public static formatHttpContent(content: string, isRequest: boolean, isResponse: boolean): string {
+    if (!content || content.trim() === '') {
+      return '<div class="empty-content">No content available</div>';
+    }
+    
+    try {
+      const lines = content.split('\n');
+      let formattedHtml = '';
+      
+      // Format the first line differently (status line or request line)
+      if (lines.length > 0) {
+        const firstLine = lines[0].trim();
+        
+        if (isRequest) {
+          // Format request line (e.g., "GET /path HTTP/1.1")
+          const parts = firstLine.split(' ');
+          if (parts.length >= 3) {
+            const [method, path, httpVersion] = parts;
+            formattedHtml += `<div class="http-first-line">
+              <span class="http-method">${this.escapeHtml(method)}</span>
+              <span class="http-path">${this.escapeHtml(path)}</span>
+              <span class="http-version">${this.escapeHtml(httpVersion)}</span>
+            </div>`;
+          } else {
+            formattedHtml += `<div class="http-first-line">${this.escapeHtml(firstLine)}</div>`;
+          }
+        } else if (isResponse) {
+          // Format status line (e.g., "HTTP/1.1 200 OK")
+          const parts = firstLine.split(' ');
+          if (parts.length >= 3) {
+            const version = parts[0];
+            const status = parts[1];
+            const statusText = parts.slice(2).join(' ');
+            
+            let statusClass = 'success';
+            if (status.startsWith('4') || status.startsWith('5')) {
+              statusClass = 'error';
+            } else if (status.startsWith('3')) {
+              statusClass = 'redirect';
+            }
+            
+            formattedHtml += `<div class="http-first-line">
+              <span class="http-version">${this.escapeHtml(version)}</span>
+              <span class="http-status ${statusClass}">${this.escapeHtml(status)}</span>
+              <span class="http-status-text">${this.escapeHtml(statusText)}</span>
+            </div>`;
+          } else {
+            formattedHtml += `<div class="http-first-line">${this.escapeHtml(firstLine)}</div>`;
+          }
+        } else {
+          formattedHtml += `<div class="http-first-line">${this.escapeHtml(firstLine)}</div>`;
+        }
+      }
+      
+      // Process headers and body
+      let inHeaders = true;
+      let headerHtml = '<div class="http-headers">';
+      let bodyHtml = '<div class="http-body">';
+      
+      for (let i = 1; i < lines.length; i++) {
+        const line = lines[i];
+        
+        if (inHeaders && line.trim() === '') {
+          inHeaders = false;
+          headerHtml += '</div>';
+          continue;
+        }
+        
+        if (inHeaders) {
+          // Process header line
+          const colonIndex = line.indexOf(':');
+          if (colonIndex > 0) {
+            const name = line.substring(0, colonIndex).trim();
+            const value = line.substring(colonIndex + 1).trim();
+            
+            headerHtml += `<div class="http-header">
+              <span class="http-header-name">${this.escapeHtml(name)}:</span>
+              <span class="http-header-value">${this.escapeHtml(value)}</span>
+            </div>`;
+          } else {
+            headerHtml += `<div class="http-header">${this.escapeHtml(line)}</div>`;
+          }
+        } else {
+          // Process body line
+          bodyHtml += `${this.escapeHtml(line)}\n`;
+        }
+      }
+      
+      if (inHeaders) {
+        headerHtml += '</div>';
+      }
+      
+      bodyHtml += '</div>';
+      
+      // Try to detect and format JSON body
+      if (!inHeaders) {
+        try {
+          // Extract body content
+          const bodyContent = lines.slice(lines.findIndex(line => line.trim() === '') + 1).join('\n');
+          if (bodyContent.trim().startsWith('{') || bodyContent.trim().startsWith('[')) {
+            const jsonData = JSON.parse(bodyContent);
+            const prettyJson = JSON.stringify(jsonData, null, 2);
+            
+            bodyHtml = `<div class="http-body json">
+              <pre class="formatted-json">${this.escapeHtml(prettyJson)}</pre>
+            </div>`;
+          }
+        } catch (e) {
+          // Not valid JSON, keep the original body
+        }
+      }
+      
+      return formattedHtml + headerHtml + bodyHtml;
+    } catch (error) {
+      console.error('Error formatting HTTP content:', error);
+      return `<div class="error-content">Error formatting content: ${error instanceof Error ? error.message : String(error)}</div>`;
+    }
+  }
+  
+  /**
+   * Escape HTML special characters to prevent XSS
+   * @param text The text to escape
+   * @returns Escaped HTML
+   */
+  private static escapeHtml(text: string): string {
+    return text
+      .replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;')
+      .replace(/'/g, '&#039;');
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/HttpService.ts b/client/src/services/HttpService.ts
index 1fd590d..d708926 100644
--- a/client/src/services/HttpService.ts
+++ b/client/src/services/HttpService.ts
@@ -1,8 +1,11 @@
 /**
  * HttpService.ts
- * Handles HTTP-related operations and cryptography functions for the HTTP-to-Nostr application
+ * Handles HTTP-related operations for the HTTP-to-Nostr application
  */
 
+// Import crypto utilities
+import { encryptWithWebCrypto, decryptWithWebCrypto } from '../utils/crypto-utils';
+
 // Interface for HTTP request options
 export interface HttpRequestOptions {
   method: string;
@@ -16,8 +19,6 @@ export interface HttpRequestOptions {
 export class HttpService {
   /**
    * Parse a raw HTTP request text into its components
-   * @param httpRequestText The raw HTTP request text
-   * @returns Parsed HTTP request details or null if invalid
    */
   public parseHttpRequest(httpRequestText: string): { url: string; options: HttpRequestOptions } | null {
     try {
@@ -33,11 +34,11 @@ export class HttpService {
       const method = requestLine[0];
       let url = requestLine[1];
       
-      // If it's a relative URL, make it absolute
+      // Handle URL format
       if (url.startsWith('/')) {
         url = `${window.location.origin}${url}`;
       } else if (!url.startsWith('http')) {
-        url = `http://${url}`; // Default to http if no protocol specified
+        url = `http://${url}`;
       }
       
       // Parse headers
@@ -62,11 +63,8 @@ export class HttpService {
         i++;
       }
       
-      // Extract body if present
-      let body = '';
-      if (bodyStartIndex < lines.length) {
-        body = lines.slice(bodyStartIndex).join('\n');
-      }
+      // Extract body
+      const body = bodyStartIndex < lines.length ? lines.slice(bodyStartIndex).join('\n') : '';
       
       // Return the parsed request
       return {
@@ -84,8 +82,6 @@ export class HttpService {
 
   /**
    * Execute an HTTP request
-   * @param httpRequestText The raw HTTP request text
-   * @returns A promise that resolves to the HTTP response as text
    */
   public async executeHttpRequest(httpRequestText: string): Promise<string> {
     try {
@@ -95,132 +91,38 @@ export class HttpService {
       }
       
       const { url, options } = parsedRequest;
-      
-      // Use the fetch API to execute the request - explicitly using window.fetch
       const response = await window.fetch(url, options);
       
       // Prepare the response text
       let responseText = `HTTP/1.1 ${response.status} ${response.statusText}\n`;
       
-      // Add response headers
+      // Add headers and body
       response.headers.forEach((value: string, key: string) => {
         responseText += `${key}: ${value}\n`;
       });
       
-      // Add empty line to separate headers from body
       responseText += '\n';
-      
-      // Add response body
-      const responseBody = await response.text();
-      responseText += responseBody;
+      responseText += await response.text();
       
       return responseText;
     } catch (error) {
-      if (error instanceof Error) {
-        return `Error executing HTTP request: ${error.message}`;
-      } else {
-        return `Error executing HTTP request: ${String(error)}`;
-      }
+      return `Error executing HTTP request: ${error instanceof Error ? error.message : String(error)}`;
     }
   }
 
   /**
-   * Encrypt data using Web Crypto API
-   * @param plaintext The plaintext to encrypt
-   * @param key The encryption key
-   * @returns A promise that resolves to a base64-encoded encrypted string
+   * Crypto utility: Encrypt data using Web Crypto API
+   * Uses the shared crypto-utils implementation
    */
-  public async encryptWithWebCrypto(plaintext: string, key: string): Promise<string> {
-    try {
-      // Generate a random IV
-      const iv = crypto.getRandomValues(new Uint8Array(12));
-      
-      // Create key material from the encryption key
-      const keyMaterial = await crypto.subtle.digest(
-        'SHA-256',
-        new TextEncoder().encode(key)
-      );
-      
-      // Import the key for AES-GCM encryption
-      const cryptoKey = await crypto.subtle.importKey(
-        'raw',
-        keyMaterial,
-        { name: 'AES-GCM' },
-        false,
-        ['encrypt']
-      );
-      
-      // Encrypt the data
-      const encryptedData = await crypto.subtle.encrypt(
-        {
-          name: 'AES-GCM',
-          iv: iv
-        },
-        cryptoKey,
-        new TextEncoder().encode(plaintext)
-      );
-      
-      // Combine IV and ciphertext
-      const encryptedBytes = new Uint8Array(iv.length + new Uint8Array(encryptedData).length);
-      encryptedBytes.set(iv, 0);
-      encryptedBytes.set(new Uint8Array(encryptedData), iv.length);
-      
-      // Convert to base64
-      return btoa(String.fromCharCode(...encryptedBytes));
-    } catch (error) {
-      throw new Error(`WebCrypto encryption failed: ${error instanceof Error ? error.message : String(error)}`);
-    }
+  public async encryptData(plaintext: string, key: string): Promise<string> {
+    return encryptWithWebCrypto(plaintext, key);
   }
 
   /**
-   * Decrypt data using Web Crypto API
-   * @param encryptedBase64 The base64-encoded encrypted data
-   * @param key The decryption key
-   * @returns A promise that resolves to the decrypted plaintext
+   * Crypto utility: Decrypt data using Web Crypto API
+   * Uses the shared crypto-utils implementation
    */
-  public async decryptWithWebCrypto(encryptedBase64: string, key: string): Promise<string> {
-    try {
-      // Convert base64 to byte array
-      const encryptedBytes = new Uint8Array(
-        atob(encryptedBase64)
-          .split('')
-          .map(char => char.charCodeAt(0))
-      );
-      
-      // Extract IV (first 12 bytes)
-      const iv = encryptedBytes.slice(0, 12);
-      // Extract ciphertext (remaining bytes)
-      const ciphertext = encryptedBytes.slice(12);
-      
-      // Create key material from the decryption key
-      const keyMaterial = await crypto.subtle.digest(
-        'SHA-256',
-        new TextEncoder().encode(key)
-      );
-      
-      // Import the key for AES-GCM decryption
-      const cryptoKey = await crypto.subtle.importKey(
-        'raw',
-        keyMaterial,
-        { name: 'AES-GCM' },
-        false,
-        ['decrypt']
-      );
-      
-      // Decrypt the data
-      const decryptedData = await crypto.subtle.decrypt(
-        {
-          name: 'AES-GCM',
-          iv: iv
-        },
-        cryptoKey,
-        ciphertext
-      );
-      
-      // Convert decrypted data to string
-      return new TextDecoder().decode(decryptedData);
-    } catch (error) {
-      throw new Error(`WebCrypto decryption failed: ${error instanceof Error ? error.message : String(error)}`);
-    }
+  public async decryptData(encryptedBase64: string, key: string): Promise<string> {
+    return decryptWithWebCrypto(encryptedBase64, key);
   }
 }
\ No newline at end of file
diff --git a/client/src/services/Nostr21121EventHandler.ts b/client/src/services/Nostr21121EventHandler.ts
new file mode 100644
index 0000000..6beae7e
--- /dev/null
+++ b/client/src/services/Nostr21121EventHandler.ts
@@ -0,0 +1,92 @@
+/**
+ * Nostr21121EventHandler.ts
+ * Handles NIP-21121 HTTP response events
+ */
+
+import { NostrEvent } from '../relay';
+import { NostrService } from './NostrService';
+
+/**
+ * Class for handling NIP-21121 HTTP response events
+ */
+export class Nostr21121EventHandler {
+  private nostrService: NostrService;
+  private responseEventMap: Map<string, string[]> = new Map();
+  
+  /**
+   * Constructor
+   * @param nostrService The NostrService instance
+   */
+  constructor(nostrService: NostrService) {
+    this.nostrService = nostrService;
+  }
+  
+  /**
+   * Find a 21121 response event for a specific 21120 request
+   * @param requestEventId The ID of the 21120 request event
+   * @returns Promise resolving to the response event or null if not found
+   */
+  public async findResponseForRequest(requestEventId: string): Promise<NostrEvent | null> {
+    try {
+      if (!requestEventId) {
+        console.error('Invalid request event ID');
+        return null;
+      }
+      
+      // Get active relay
+      const relayService = this.nostrService.getRelayService();
+      const relayUrl = relayService.getActiveRelayUrl();
+      
+      if (!relayUrl) {
+        console.error('No active relay available');
+        return null;
+      }
+      
+      // Simplified implementation - instead of actual relay queries
+      console.log(`Searching for response event for request ${requestEventId} on relay ${relayUrl}`);
+      
+      // Simulate searching for events - in a real implementation this would query relays
+      return new Promise((resolve) => {
+        setTimeout(() => {
+          console.log('No response event found - this is a simplified implementation');
+          resolve(null);
+        }, 1000);
+      });
+    } catch (error) {
+      console.error('Error finding 21121 response:', error);
+      return null;
+    }
+  }
+  
+  /**
+   * Add a related event to the map
+   * @param requestId The 21120 request event ID
+   * @param responseId The 21121 response event ID
+   */
+  public addRelatedEvent(requestId: string, responseId: string): void {
+    const existing = this.responseEventMap.get(requestId) || [];
+    if (!existing.includes(responseId)) {
+      existing.push(responseId);
+      this.responseEventMap.set(requestId, existing);
+    }
+  }
+  
+  /**
+   * Get related events for a given event ID
+   * @param eventId The event ID to get related events for
+   * @returns Array of related event IDs
+   */
+  public getRelatedEvents(eventId: string): string[] {
+    return this.responseEventMap.get(eventId) || [];
+  }
+  
+  /**
+   * Check if an event has related 21121 responses
+   * @param eventId The event ID to check
+   * @returns True if the event has responses, false otherwise
+   */
+  public hasRelatedResponses(eventId: string): boolean {
+    const related = this.responseEventMap.get(eventId);
+    return related !== undefined && related.length > 0;
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/Nostr21121IntegrationHelper.ts b/client/src/services/Nostr21121IntegrationHelper.ts
new file mode 100644
index 0000000..12c0244
--- /dev/null
+++ b/client/src/services/Nostr21121IntegrationHelper.ts
@@ -0,0 +1,304 @@
+/**
+ * Nostr21121IntegrationHelper
+ * Handles integration between HTTP content and NIP-21121 events
+ */
+
+import { NostrEvent } from '../../src/relay';
+import { Nostr21121Service } from './Nostr21121Service';
+import { NostrEventService } from './NostrEventService';
+import { ToastNotifier } from './ToastNotifier';
+import { HttpFormatter } from './HttpFormatter';
+
+/**
+ * Helper class for NIP-21121 integration with HTTP content
+ */
+export class Nostr21121IntegrationHelper {
+  private nostr21121Service: Nostr21121Service;
+  private nostrEventService: NostrEventService;
+  private httpResponseModal: HTMLElement | null = null;
+  
+  constructor(nostr21121Service: Nostr21121Service, nostrEventService: NostrEventService) {
+    this.nostr21121Service = nostr21121Service;
+    this.nostrEventService = nostrEventService;
+    
+    // Initialize the handlers when the class is instantiated
+    this.initializeEventListeners();
+  }
+  
+  /**
+   * Initialize event listeners for HTTP request execution
+   */
+  private initializeEventListeners(): void {
+    // Click handler for the execute button
+    document.addEventListener('click', async (event) => {
+      const target = event.target as HTMLElement;
+      
+      // Handle HTTP request execution button
+      if (target && (
+        target.classList.contains('execute-http-request-btn') || 
+        target.closest('.execute-http-request-btn')
+      )) {
+        const button = target.classList.contains('execute-http-request-btn') ? 
+          target : target.closest('.execute-http-request-btn') as HTMLElement;
+        
+        // Don't do anything if button is disabled
+        if (button.hasAttribute('disabled')) {
+          return;
+        }
+        
+        await this.handleHttpRequestExecution(button);
+      }
+    });
+  }
+  
+  /**
+   * Handle HTTP request execution
+   * @param target The button element that was clicked
+   */
+  private async handleHttpRequestExecution(target: HTMLElement): Promise<void> {
+    // Find the event details container
+    const eventDetails = target.closest('.event-details');
+    if (!eventDetails) {
+      console.error('Event details container not found');
+      return;
+    }
+    
+    try {
+      // Extract the event ID from the header
+      const eventIdMatch = eventDetails.querySelector('h3')?.textContent?.match(/ID: (\w+)\.\.\./);
+      if (!eventIdMatch || !eventIdMatch[1]) return;
+      
+      const eventId = eventIdMatch[1];
+      
+      try {
+        // Get the event directly from the DOM
+        const rawJsonElement = eventDetails.querySelector('#raw-json pre');
+        if (!rawJsonElement || !rawJsonElement.textContent) {
+          throw new Error('Raw JSON not found');
+        }
+        
+        const eventData = JSON.parse(rawJsonElement.textContent) as NostrEvent;
+        
+        // Get the HTTP content
+        const httpContent = eventDetails.querySelector('.http-content')?.textContent;
+        if (!httpContent) return;
+        
+        // Execute the HTTP request
+        try {
+          // Disable the button and show loading state
+          target.textContent = 'Executing...';
+          target.setAttribute('disabled', 'true');
+          
+          // Execute the request
+          const response = await executeHttpRequestWithFetch(httpContent);
+          
+          // Show the response in a modal
+          this.displayHttpResponse(response);
+          
+          // Ask if user wants to create a 21121 response
+          if (confirm('Do you want to create and publish a NIP-21121 response event?')) {
+            await this.createAndPublish21121Response(eventData, response);
+          }
+          
+        } catch (error) {
+          console.error('Error executing HTTP request:', error);
+          ToastNotifier.show(`Error: ${error instanceof Error ? error.message : String(error)}`, 'error');
+        } finally {
+          // Restore the button
+          target.textContent = 'Execute HTTP Request';
+          target.removeAttribute('disabled');
+        }
+        
+      } catch (error) {
+        console.error('Error parsing event data:', error);
+        ToastNotifier.show(`Error: Could not parse event data - ${error instanceof Error ? error.message : String(error)}`, 'error');
+      }
+      
+    } catch (error) {
+      console.error('Error in HTTP execution handler:', error);
+      ToastNotifier.show(`Error: ${error instanceof Error ? error.message : String(error)}`, 'error');
+    }
+  }
+  
+  /**
+   * Display HTTP response in the modal
+   * @param responseContent The HTTP response content
+   */
+  private displayHttpResponse(responseContent: string): void {
+    // Create or get the modal
+    let modal = document.getElementById('httpResponseModal');
+    if (!modal) {
+      console.error('HTTP response modal not found in DOM');
+      return;
+    }
+    
+    // Get content containers
+    const formattedContainer = modal.querySelector('#formatted-response .http-formatted-container');
+    const rawContainer = modal.querySelector('#raw-response pre');
+    
+    // Update the content
+    if (formattedContainer) {
+      formattedContainer.innerHTML = HttpFormatter.formatHttpContent(responseContent, false, true);
+    }
+    
+    if (rawContainer) {
+      rawContainer.textContent = responseContent;
+    }
+    
+    // Show the modal
+    (modal as HTMLElement).style.display = 'block';
+    
+    // Listen for tab buttons within this modal
+    const tabButtons = modal.querySelectorAll('.tab-btn');
+    tabButtons.forEach(button => {
+      button.addEventListener('click', () => {
+        // Get the target tab
+        const targetTabId = (button as HTMLElement).dataset.tab;
+        if (!targetTabId) {
+          return;
+        }
+        
+        // Remove active class from all buttons and content
+        tabButtons.forEach(btn => btn.classList.remove('active'));
+        modal?.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
+        
+        // Add active class to clicked tab
+        button.classList.add('active');
+        
+        // Show corresponding content
+        const content = modal?.querySelector(`#${targetTabId}`);
+        if (content) {
+          content.classList.add('active');
+        }
+      });
+    });
+  }
+  
+  /**
+   * Create and publish a 21121 response event
+   * @param requestEvent The original 21120 request event
+   * @param responseContent The HTTP response content
+   */
+  private async createAndPublish21121Response(requestEvent: NostrEvent, responseContent: string): Promise<void> {
+    try {
+      // Get the server's private key
+      const serverNsec = localStorage.getItem('serverNsec');
+      if (!serverNsec) {
+        ToastNotifier.show('Server private key (nsec) not found. Please set up a server identity first.', 'error');
+        return;
+      }
+      
+      // Get the relay URL from the UI
+      const relayUrlInput = document.getElementById('relayUrl') as HTMLInputElement;
+      const relayUrl = relayUrlInput?.value || 'wss://relay.degmods.com'; 
+      
+      // Create and publish the 21121 event
+      const responseEvent = await this.nostr21121Service.createAndPublish21121Event(
+        requestEvent,
+        responseContent,
+        serverNsec,
+        relayUrl
+      );
+      
+      if (responseEvent) {
+        ToastNotifier.show('NIP-21121 response event published successfully!', 'success');
+        
+        // Add a visual indicator to the event in the UI
+        const eventItem = document.querySelector(`.event-item[data-id="${requestEvent.id}"]`);
+        if (eventItem && !eventItem.querySelector('.response-indicator')) {
+          const responseIndicator = document.createElement('div');
+          responseIndicator.className = 'response-indicator';
+          responseIndicator.innerHTML = '<span class="response-available">21121 Response Available</span>';
+          eventItem.appendChild(responseIndicator);
+        }
+      } else {
+        ToastNotifier.show('Failed to publish NIP-21121 response event', 'error');
+      }
+    } catch (error) {
+      console.error('Error creating 21121 response:', error);
+      ToastNotifier.show(`Error: ${error instanceof Error ? error.message : String(error)}`, 'error');
+    }
+  }
+}
+
+/**
+ * Execute HTTP request using fetch API
+ * @param requestContent The HTTP request content
+ * @returns Promise resolving to the HTTP response
+ */
+async function executeHttpRequestWithFetch(requestContent: string): Promise<string> {
+  // Parse the request
+  const lines = requestContent.trim().split('\n');
+  if (lines.length === 0) {
+    throw new Error('Empty request');
+  }
+
+  // Parse the first line (e.g., "GET /api/users HTTP/1.1")
+  const firstLine = lines[0].trim();
+  const [method, path, _httpVersion] = firstLine.split(' ');
+
+  // Extract the host
+  let host = '';
+  let headers: Record<string, string> = {};
+  let body = '';
+  let inHeaders = true;
+
+  for (let i = 1; i < lines.length; i++) {
+    const line = lines[i].trim();
+    
+    if (inHeaders && line === '') {
+      inHeaders = false;
+      continue;
+    }
+    
+    if (inHeaders) {
+      const colonIndex = line.indexOf(':');
+      if (colonIndex > 0) {
+        const key = line.substring(0, colonIndex).trim();
+        const value = line.substring(colonIndex + 1).trim();
+        headers[key.toLowerCase()] = value;
+        
+        if (key.toLowerCase() === 'host') {
+          host = value;
+        }
+      }
+    } else {
+      body += line + '\n';
+    }
+  }
+
+  if (!host) {
+    throw new Error('Host header is required');
+  }
+
+  // Construct URL
+  const isSecure = host.includes(':443') || !host.includes(':');
+  const protocol = isSecure ? 'https://' : 'http://';
+  const baseUrl = protocol + host.split(':')[0];
+  const url = new URL(path, baseUrl).toString();
+
+  // Perform fetch
+  try {
+    const response = await fetch(url, {
+      method,
+      headers,
+      body: body.trim() || undefined,
+    });
+
+    // Build response string
+    let responseText = `HTTP/1.1 ${response.status} ${response.statusText}\n`;
+    
+    // Add headers
+    response.headers.forEach((value, key) => {
+      responseText += `${key}: ${value}\n`;
+    });
+    
+    // Add body
+    const responseBody = await response.text();
+    responseText += '\n' + responseBody;
+    
+    return responseText;
+  } catch (error) {
+    return `HTTP/1.1 500 Internal Server Error\nContent-Type: text/plain\n\nError: ${error instanceof Error ? error.message : String(error)}`;
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/Nostr21121ResponseHandler.ts b/client/src/services/Nostr21121ResponseHandler.ts
new file mode 100644
index 0000000..15d5977
--- /dev/null
+++ b/client/src/services/Nostr21121ResponseHandler.ts
@@ -0,0 +1,136 @@
+/**
+ * Nostr21121ResponseHandler.ts
+ * Handler for creating and managing NIP-21121 HTTP response events
+ */
+
+import { NostrEvent } from '../relay';
+import { NostrService } from './NostrService';
+import { ToastNotifier } from './ToastNotifier';
+
+/**
+ * Class for handling NIP-21121 HTTP response events
+ */
+export class Nostr21121ResponseHandler {
+  private nostrService: NostrService;
+  private relatedEvents: Map<string, string[]>;
+  
+  /**
+   * Constructor
+   * @param nostrService The NostrService instance
+   * @param relatedEvents Map of related events
+   */
+  constructor(nostrService: NostrService, relatedEvents: Map<string, string[]>) {
+    this.nostrService = nostrService;
+    this.relatedEvents = relatedEvents;
+  }
+  
+  /**
+   * Create and publish a NIP-21121 response event
+   * @param requestEvent The original 21120 request event
+   * @param responseContent The HTTP response content
+   * @returns Promise resolving to success status
+   */
+  public async createAndPublish21121Response(requestEvent: NostrEvent, responseContent: string): Promise<boolean> {
+    try {
+      // Get the server private key
+      const serverNsec = localStorage.getItem('serverNsec');
+      if (!serverNsec) {
+        ToastNotifier.show('Server private key (nsec) not found. Please set up a server identity first.', 'error');
+        return false;
+      }
+      
+      // Get the active relay URL
+      const relayUrl = this.nostrService.getRelayService().getActiveRelayUrl();
+      if (!relayUrl) {
+        ToastNotifier.show('No active relay connection. Please connect to a relay first.', 'error');
+        return false;
+      }
+      
+      // Create and publish the 21121 event
+      ToastNotifier.show('Creating and publishing 21121 response event...', 'info');
+      
+      const responseEvent = await this.nostrService.createAndPublish21121Event(
+        requestEvent,
+        responseContent,
+        serverNsec,
+        relayUrl
+      );
+      
+      if (responseEvent && responseEvent.id && requestEvent.id) {
+        // Add the response to related events mapping
+        this.addRelatedEvent(requestEvent.id, responseEvent.id);
+        
+        // Notify user
+        ToastNotifier.show('NIP-21121 response event published successfully!', 'success');
+        
+        // Add indicator to UI
+        this.addResponseIndicator(requestEvent.id);
+        
+        return true;
+      } else {
+        ToastNotifier.show('Failed to publish NIP-21121 response event', 'error');
+        return false;
+      }
+    } catch (error) {
+      console.error('Error creating 21121 response:', error);
+      ToastNotifier.show(`Error: ${error instanceof Error ? error.message : String(error)}`, 'error');
+      return false;
+    }
+  }
+  
+  /**
+   * Add a related event to the map
+   * @param requestId The request event ID
+   * @param responseId The response event ID
+   */
+  private addRelatedEvent(requestId: string, responseId: string): void {
+    const relatedIds = this.relatedEvents.get(requestId) || [];
+    if (!relatedIds.includes(responseId)) {
+      relatedIds.push(responseId);
+      this.relatedEvents.set(requestId, relatedIds);
+    }
+  }
+  
+  /**
+   * Add a visual indicator to the event in the UI
+   * @param requestEventId The request event ID
+   */
+  private addResponseIndicator(requestEventId: string): void {
+    const eventItem = document.querySelector(`.event-item[data-id="${requestEventId}"]`);
+    if (eventItem && !eventItem.querySelector('.response-indicator')) {
+      const responseIndicator = document.createElement('div');
+      responseIndicator.className = 'response-indicator';
+      responseIndicator.innerHTML = '<span class="response-available">21121 Response Available</span>';
+      eventItem.appendChild(responseIndicator);
+    }
+  }
+  
+  /**
+   * Find responses for a given request event
+   * @param requestEventId The request event ID
+   * @param relayUrl The relay URL to search
+   * @returns Promise resolving to the response event or null if not found
+   */
+  public async findResponseForRequest(requestEventId: string, relayUrl: string): Promise<NostrEvent | null> {
+    return this.nostrService.findResponseForRequest(requestEventId, relayUrl);
+  }
+  
+  /**
+   * Check if a request has responses
+   * @param eventId The event ID to check
+   * @returns True if the event has responses, false otherwise
+   */
+  public hasResponses(eventId: string): boolean {
+    const relatedIds = this.relatedEvents.get(eventId) || [];
+    return relatedIds.length > 0;
+  }
+  
+  /**
+   * Get related response events for a request
+   * @param eventId The request event ID
+   * @returns Array of related response event IDs
+   */
+  public getResponsesForRequest(eventId: string): string[] {
+    return this.relatedEvents.get(eventId) || [];
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/Nostr21121Service.ts b/client/src/services/Nostr21121Service.ts
new file mode 100644
index 0000000..6221bfc
--- /dev/null
+++ b/client/src/services/Nostr21121Service.ts
@@ -0,0 +1,121 @@
+/**
+ * NIP-21121 Service for HTTP response events
+ */
+
+import { NostrEvent } from '../relay';
+import * as nostrTools from 'nostr-tools';
+
+/**
+ * Service for managing NIP-21121 HTTP response events
+ */
+export class Nostr21121Service {
+  private relayService: any;
+  private cacheService: any;
+  
+  constructor(relayService: any, cacheService: any) {
+    this.relayService = relayService;
+    this.cacheService = cacheService;
+  }
+  
+  /**
+   * Create and publish a NIP-21121 HTTP response event
+   * @param requestEvent The original 21120 request event
+   * @param responseContent The HTTP response content
+   * @param serverPrivateKey The server's private key for signing
+   * @param relayUrl The relay URL to publish to
+   * @returns The created and published event
+   */
+  public async createAndPublish21121Event(
+    requestEvent: NostrEvent,
+    responseContent: string,
+    serverPrivateKey: Uint8Array | string,
+    relayUrl: string
+  ): Promise<NostrEvent | null> {
+    try {
+      // Ensure we have a valid request event
+      if (!requestEvent || !requestEvent.id) {
+        throw new Error('Invalid request event');
+      }
+      
+      // Extract private key
+      let privateKeyStr = typeof serverPrivateKey === 'string' ? serverPrivateKey : '';
+      if (typeof serverPrivateKey === 'string' && serverPrivateKey.startsWith('nsec')) {
+        try {
+          // This would normally decode the nsec, but we'll just simulate it
+          console.log('Would decode nsec to hex');
+          privateKeyStr = serverPrivateKey;
+        } catch (e) {
+          console.error('Error decoding nsec:', e);
+          return null;
+        }
+      }
+      
+      // Get the public key from the private key
+      const pubKey = nostrTools.getPublicKey(privateKeyStr as any);
+      
+      // Check if we need to encrypt the response
+      let finalContent = responseContent;
+      let tags: string[][] = [];
+      
+      // Always add reference to the request event
+      if (requestEvent.id) {
+        tags.push(['e', requestEvent.id, '']);
+      }
+      
+      // Add kind reference
+      tags.push(['k', '21120']);
+            
+      // Check if the original event has a p tag (recipient)
+      const pTag = requestEvent.tags.find(tag => tag[0] === 'p');
+      if (pTag && pTag[1]) {
+        // This would be encrypted in a real implementation
+        console.log('Would encrypt content for recipient:', pTag[1]);
+        
+        // Add p tag to reference the recipient
+        tags.push(['p', pTag[1], '']);
+      }
+      
+      // Create the event
+      const eventBody = {
+        kind: 21121,
+        created_at: Math.floor(Date.now() / 1000),
+        tags: tags,
+        content: finalContent,
+        pubkey: pubKey
+      };
+      
+      // Compute the event ID (hash)
+      const id = nostrTools.getEventHash(eventBody);
+      
+      // Sign the event (simplified)
+      const sig = 'simulated_signature_for_demo';
+      
+      // Create the complete signed event
+      const event: NostrEvent = {
+        ...eventBody,
+        id,
+        sig
+      };
+      
+      // Simulate publishing to relay
+      console.log(`Would publish event to relay: ${relayUrl}`);
+      console.log('Event:', event);
+      
+      return event;
+    } catch (error) {
+      console.error('Error creating 21121 event:', error);
+      return null;
+    }
+  }
+  
+  /**
+   * Find a response event for a request
+   * @param requestEventId The ID of the request event to find responses for
+   * @param relayUrl The relay URL to search
+   * @returns Promise resolving to the response event or null if not found
+   */
+  public findResponseForRequest(requestEventId: string, relayUrl: string): Promise<NostrEvent | null> {
+    console.log(`Would search for response to ${requestEventId} on ${relayUrl}`);
+    return Promise.resolve(null);
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/Nostr31120Service.ts b/client/src/services/Nostr31120Service.ts
new file mode 100644
index 0000000..ec4cf30
--- /dev/null
+++ b/client/src/services/Nostr31120Service.ts
@@ -0,0 +1,409 @@
+/**
+ * Nostr31120Service.ts
+ * Dedicated service for handling kind 31120 events
+ */
+
+// External imports
+import * as nostrTools from 'nostr-tools';
+
+import type { NostrEvent } from '../relay';
+import type { NostrFilter } from './NostrEventService';
+import type { NostrRelayService } from './NostrRelayService';
+import type { NostrCacheService } from './NostrCacheService';
+
+/**
+ * Service for working with kind 31120 events (HTTP-over-Nostr server registrations)
+ */
+export class Nostr31120Service {
+  private relayService: NostrRelayService;
+  private cacheService: NostrCacheService;
+
+  /**
+   * Create a new 31120 service
+   */
+  constructor(relayService: NostrRelayService, cacheService: NostrCacheService) {
+    this.relayService = relayService;
+    this.cacheService = cacheService;
+  }
+
+  /**
+   * Query for all kind 31120 events from a relay
+   * @param relayUrl The relay URL to query
+   * @returns Promise resolving to an array of 31120 events
+   */
+  public async queryForAll31120Events(relayUrl: string): Promise<NostrEvent[]> {
+    console.log(`Querying for all kind 31120 events from ${relayUrl}`);
+    
+    try {
+      // Connect to relay if needed
+      if (!this.relayService.isConnected() || this.relayService.getActiveRelayUrl() !== relayUrl) {
+        await this.relayService.connectToRelay(relayUrl);
+      }
+      
+      // Check the cache first
+      const cachedEvents = this.cacheService.getCachedEvents(relayUrl);
+      if (cachedEvents && cachedEvents.length > 0) {
+        // Filter cached events for kind 31120
+        const filteredEvents = cachedEvents.filter(event => event.kind === 31120);
+        if (filteredEvents.length > 0) {
+          console.log(`Using ${filteredEvents.length} cached 31120 events`);
+          return filteredEvents;
+        }
+      }
+      
+      // Create filter for all kind 31120 events
+      const filter: NostrFilter = {
+        kinds: [31120],
+        limit: 100  // Reasonable limit for server advertisements
+      };
+      
+      // Set a timeout for the query
+      const timeoutMs = 8000;
+      const timeoutPromise = new Promise<NostrEvent[]>((_, reject) => {
+        setTimeout(() => reject(new Error(`Query timed out after ${timeoutMs}ms`)), timeoutMs);
+      });
+      
+      // Create a promise that will resolve with the query results
+      const queryPromise = new Promise<NostrEvent[]>((resolve) => {
+        const events: NostrEvent[] = [];
+        const reqId = `req-${Date.now()}`;
+        
+        // Create a new connection with custom message handler
+        (async () => {
+          try {
+            // Ensure we're connected to the relay
+            const connected = await this.relayService.connectToRelay(relayUrl);
+            if (!connected) {
+              resolve([]);
+              return;
+            }
+            
+            // Get the WebSocket manager
+            const wsManager = this.relayService.getWebSocketManager();
+            
+            // Connect with custom message handler
+            await wsManager.connect(relayUrl, {
+              onMessage: (data: unknown) => {
+                try {
+                  const message = data as unknown[];
+                  if (Array.isArray(message) && message[0] === "EVENT" && message[1] === reqId) {
+                    // Add the event to our results
+                    const event = message[2] as NostrEvent;
+                    events.push(event);
+                  } else if (Array.isArray(message) && message[0] === "EOSE" && message[1] === reqId) {
+                    // End of stored events, resolve the promise
+                    resolve(events);
+                  }
+                } catch (error) {
+                  console.error("Error processing message:", error);
+                }
+              }
+            });
+            
+            // Send the query to the relay
+            const reqMsg = JSON.stringify(["REQ", reqId, filter]);
+            wsManager.send(reqMsg);
+          } catch (error) {
+            console.error("Error setting up event query:", error);
+            resolve([]);
+          }
+        })();
+      });
+      
+      // Execute the query with a timeout
+      let events: NostrEvent[];
+      try {
+        events = await Promise.race([queryPromise, timeoutPromise]);
+      } catch (error) {
+        console.error("Query timed out or failed:", error);
+        // Return cached events if any
+        return cachedEvents?.filter(event => event.kind === 31120) || [];
+      }
+      
+      // Filter out expired events
+      const now = Math.floor(Date.now() / 1000);
+      const validEvents = events.filter(event => {
+        // Check for expiry tag
+        const expiryTag = event.tags.find(tag => tag[0] === 'expiry');
+        if (expiryTag && expiryTag.length > 1) {
+          const expiryTime = parseInt(expiryTag[1]);
+          if (!isNaN(expiryTime) && expiryTime < now) {
+            return false; // Skip expired events
+          }
+        }
+        return true;
+      });
+      
+      // Cache the valid results
+      if (validEvents.length > 0) {
+        this.cacheService.cacheEvents(relayUrl, validEvents);
+      }
+      
+      console.log(`Found ${validEvents.length} valid 31120 events from relay (${events.length - validEvents.length} expired)`);
+      return validEvents;
+    } catch (error) {
+      console.error("Error querying for all 31120 events:", error);
+      return [];
+    }
+  }
+
+  /**
+   * Get the relevant server pubkey from a kind 31120 event
+   * @param event The 31120 event to extract pubkey from
+   * @returns Server pubkey or null if not found
+   */
+  public getServerPubkeyFromEvent(event: NostrEvent): string | null {
+    if (event.kind !== 31120) return null;
+    
+    // Find the d tag which contains the server pubkey
+    const dTag = event.tags.find(tag => tag[0] === 'd');
+    if (dTag && dTag.length > 1) {
+      return dTag[1];
+    }
+    
+    return null;
+  }
+
+  /**
+   * Create or update a 31120 event (server advertisement)
+   * @param relayUrl The relay URL to publish to
+   * @param content The content/description of the server
+   * @param relays Array of relay URLs to include as tags
+   * @param expiryHours Number of hours until expiry
+   * @param existingEventId Optional ID of an existing event to update
+   * @param customServerPubkey Optional custom server pubkey to use
+   * @returns Promise resolving to the created/updated event
+   */
+  public async createOrUpdate31120Event(
+    relayUrl: string,
+    content: string,
+    relays: string[],
+    expiryHours: number,
+    existingEventId?: string,
+    customServerPubkey?: string
+  ): Promise<{ event: NostrEvent; serverNsec?: string } | null> {
+    try {
+      // Ensure we have an active connection
+      if (!this.relayService.isConnected() || this.relayService.getActiveRelayUrl() !== relayUrl) {
+        const connected = await this.relayService.connectToRelay(relayUrl);
+        if (!connected) {
+          throw new Error(`Could not connect to relay: ${relayUrl}`);
+        }
+      }
+
+      // Check if window.nostr (Nostr browser extension) is available
+      if (!window.nostr) {
+        throw new Error('No Nostr browser extension found. Please install a NIP-07 compatible extension.');
+      }
+      
+      // Get user's public key using the extension
+      let userPubkey: string;
+      try {
+        userPubkey = await window.nostr!.getPublicKey();
+        if (!userPubkey) {
+          throw new Error('Failed to get public key from Nostr extension');
+        }
+      } catch (error) {
+        console.error('Error getting public key from Nostr extension:', error);
+        throw new Error('Failed to get your public key. Please make sure your Nostr extension is working.');
+      }
+      
+      // Generate or use a provided server pubkey
+      let serverPubkey: string;
+      if (customServerPubkey) {
+        // Convert from npub format if needed
+        if (customServerPubkey.startsWith('npub')) {
+          try {
+            const decoded = nostrTools.nip19.decode(customServerPubkey);
+            if (decoded.type === 'npub') {
+              serverPubkey = decoded.data as string;
+            } else {
+              throw new Error('Invalid server pubkey format');
+            }
+          } catch (error) {
+            console.error('Error decoding server pubkey:', error);
+            throw new Error('Could not decode server pubkey');
+          }
+        } else {
+          serverPubkey = customServerPubkey;
+        }
+      } else {
+        // Generate a proper keypair using nostr-tools
+        const secretKey = nostrTools.generateSecretKey();
+        serverPubkey = nostrTools.getPublicKey(secretKey);
+        
+        // Convert to nsec format for returning to the user
+        const nsec = nostrTools.nip19.nsecEncode(secretKey);
+        
+        // Remember the generated nsec to return later
+        return this.processEventWithServerKey(relayUrl, userPubkey, serverPubkey, content, relays, expiryHours, nsec);
+      }
+
+      // Get the current timestamp
+      const createdAt = Math.floor(Date.now() / 1000);
+      
+      // Calculate expiry time
+      const expiryTime = createdAt + (expiryHours * 3600);
+      
+      // Create tags
+      const tags: string[][] = [];
+      
+      // Add d tag with server pubkey
+      tags.push(['d', serverPubkey]);
+      
+      // Add relay tags
+      for (const relay of relays) {
+        tags.push(['relay', relay]);
+      }
+      
+      // Add expiry tag
+      tags.push(['expiry', expiryTime.toString()]);
+      
+      // Create the unsigned event
+      const unsignedEvent = {
+        kind: 31120,
+        pubkey: userPubkey,
+        created_at: createdAt,
+        tags: tags,
+        content: content || 'HTTP-over-Nostr server',
+      };
+      
+      // Sign the event using the browser extension
+      let signedEvent: NostrEvent;
+      try {
+        signedEvent = await window.nostr!.signEvent(unsignedEvent);
+        console.log('Event signed successfully with Nostr extension');
+      } catch (error) {
+        console.error('Error signing event with Nostr extension:', error);
+        throw new Error('Failed to sign the event with your Nostr extension');
+      }
+      
+      // Publish to relay
+      const relayPool = this.relayService.getRelayPool();
+      if (!relayPool) {
+        throw new Error('Relay pool not available');
+      }
+      
+      console.log('Publishing 31120 event to relay:', relayUrl);
+      try {
+        const pubs = relayPool.publish([relayUrl], signedEvent as any);
+        
+        // Wait for the publish to complete with a timeout
+        await Promise.race([
+          Promise.all(pubs),
+          new Promise((_, reject) => setTimeout(() => reject(new Error('Publish timeout')), 5000))
+        ]);
+        
+        console.log('Published 31120 event successfully');
+        
+        // Add to cache
+        this.cacheService.cacheEvents(relayUrl, [signedEvent]);
+        
+        // Return just the event without nsec for cases where a custom pubkey is used
+        return { event: signedEvent };
+      } catch (error) {
+        console.error('Failed to publish event:', error);
+        throw new Error(`Failed to publish event to relay: ${error instanceof Error ? error.message : String(error)}`);
+      }
+    } catch (error) {
+      console.error('Error creating/updating 31120 event:', error);
+      throw error;
+    }
+  }
+
+  /**
+   * Helper method to process and publish an event with a generated server key
+   * @param relayUrl The relay URL to publish to
+   * @param userPubkey The user's public key
+   * @param serverPubkey The server's public key
+   * @param content The content/description
+   * @param relays Array of relay URLs to include
+   * @param expiryHours Hours until expiry
+   * @param serverNsec The generated server's private key in nsec format
+   * @returns Promise resolving to the event and server nsec
+   */
+  private async processEventWithServerKey(
+    relayUrl: string,
+    userPubkey: string,
+    serverPubkey: string,
+    content: string,
+    relays: string[],
+    expiryHours: number,
+    serverNsec: string
+  ): Promise<{ event: NostrEvent; serverNsec: string } | null> {
+    try {
+      // Get the current timestamp
+      const createdAt = Math.floor(Date.now() / 1000);
+      
+      // Calculate expiry time
+      const expiryTime = createdAt + (expiryHours * 3600);
+      
+      // Create tags
+      const tags: string[][] = [];
+      
+      // Add d tag with server pubkey
+      tags.push(['d', serverPubkey]);
+      
+      // Add relay tags
+      for (const relay of relays) {
+        tags.push(['relay', relay]);
+      }
+      
+      // Add expiry tag
+      tags.push(['expiry', expiryTime.toString()]);
+      
+      // Create the unsigned event
+      const unsignedEvent = {
+        kind: 31120,
+        pubkey: userPubkey,
+        created_at: createdAt,
+        tags: tags,
+        content: content || 'HTTP-over-Nostr server',
+      };
+      
+      // Sign the event using the browser extension
+      let signedEvent: NostrEvent;
+      try {
+        signedEvent = await window.nostr!.signEvent(unsignedEvent);
+        console.log('Event signed successfully with Nostr extension');
+      } catch (error) {
+        console.error('Error signing event with Nostr extension:', error);
+        throw new Error('Failed to sign the event with your Nostr extension');
+      }
+      
+      // Publish to relay
+      const relayPool = this.relayService.getRelayPool();
+      if (!relayPool) {
+        throw new Error('Relay pool not available');
+      }
+      
+      console.log('Publishing 31120 event to relay:', relayUrl);
+      try {
+        const pubs = relayPool.publish([relayUrl], signedEvent as any);
+        
+        // Wait for the publish to complete with a timeout
+        await Promise.race([
+          Promise.all(pubs),
+          new Promise((_, reject) => setTimeout(() => reject(new Error('Publish timeout')), 5000))
+        ]);
+        
+        console.log('Published 31120 event successfully');
+        
+        // Add to cache
+        this.cacheService.cacheEvents(relayUrl, [signedEvent]);
+        
+        // Return the event along with the server nsec
+        return {
+          event: signedEvent,
+          serverNsec: serverNsec
+        };
+      } catch (error) {
+        console.error('Failed to publish event:', error);
+        throw new Error(`Failed to publish event to relay: ${error instanceof Error ? error.message : String(error)}`);
+      }
+    } catch (error) {
+      console.error('Error in processEventWithServerKey:', error);
+      throw error;
+    }
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/NostrCacheService.ts b/client/src/services/NostrCacheService.ts
new file mode 100644
index 0000000..599740a
--- /dev/null
+++ b/client/src/services/NostrCacheService.ts
@@ -0,0 +1,107 @@
+/**
+ * NostrCacheService.ts
+ * Handles caching of Nostr events and profile data
+ */
+
+// Project imports
+import type { NostrEvent } from '../relay';
+
+import { EventCache } from './EventCache';
+
+// Interface for profile data
+export interface ProfileData {
+  name?: string;
+  about?: string;
+  picture?: string;
+  nip05?: string;
+  [key: string]: unknown;
+}
+
+/**
+ * Class for managing Nostr caching functionality
+ */
+export class NostrCacheService {
+  private profileCache = new Map<string, ProfileData>();
+  private localStorageCachePrefix = 'nostr_31120_cache_';
+  private cacheExpiryTime = 5 * 60 * 1000; // 5 minutes
+  private eventsCache: EventCache;
+
+  /**
+   * Constructor
+   * @param cachePrefix Prefix for localStorage keys
+   * @param expiryTime Cache expiry time in milliseconds
+   */
+  constructor(cachePrefix: string = 'nostr_31120_cache_', expiryTime: number = 5 * 60 * 1000) {
+    this.localStorageCachePrefix = cachePrefix;
+    this.cacheExpiryTime = expiryTime;
+    this.eventsCache = new EventCache(cachePrefix, expiryTime);
+  }
+
+  /**
+   * Cache 31120 events by relay URL
+   * @param relayUrl The relay URL used as cache key
+   * @param events Array of 31120 events to cache
+   */
+  public cacheEvents(relayUrl: string, events: NostrEvent[]): void {
+    this.eventsCache.cacheEvents(relayUrl, events);
+  }
+
+  /**
+   * Get cached events for a relay
+   * @param relayUrl The relay URL to get cached events for
+   * @returns Array of cached events or null if cache is invalid/expired
+   */
+  public getCachedEvents(relayUrl: string): NostrEvent[] | null {
+    return this.eventsCache.getEvents(relayUrl);
+  }
+
+  /**
+   * Cache a profile for a pubkey
+   * @param pubkey The pubkey to cache for
+   * @param profile The profile data to cache
+   */
+  public cacheProfile(pubkey: string, profile: ProfileData): void {
+    this.profileCache.set(pubkey, profile);
+  }
+
+  /**
+   * Get a cached profile for a pubkey
+   * @param pubkey The pubkey to get profile for
+   * @returns The cached profile or null if not found
+   */
+  public getCachedProfile(pubkey: string): ProfileData | null {
+    return this.profileCache.get(pubkey) || null;
+  }
+
+  /**
+   * Clear the events cache for a specific relay or all relays
+   * @param relayUrl Optional relay URL to clear cache for. If not provided, clears all caches.
+   */
+  public clearEventsCache(relayUrl?: string): void {
+    if (relayUrl) {
+      this.eventsCache.clearCache(relayUrl);
+    } else {
+      this.eventsCache.clearCache();
+    }
+  }
+
+  /**
+   * Clear the profile cache for a specific pubkey or all profiles
+   * @param pubkey Optional pubkey to clear cache for. If not provided, clears all profiles.
+   */
+  public clearProfileCache(pubkey?: string): void {
+    if (pubkey) {
+      this.profileCache.delete(pubkey);
+    } else {
+      this.profileCache.clear();
+    }
+  }
+
+  /**
+   * Clear all caches (events and profiles)
+   */
+  public clearAllCaches(): void {
+    this.clearEventsCache();
+    this.clearProfileCache();
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/NostrEventService.ts b/client/src/services/NostrEventService.ts
new file mode 100644
index 0000000..5feef56
--- /dev/null
+++ b/client/src/services/NostrEventService.ts
@@ -0,0 +1,234 @@
+/**
+ * NostrEventService.ts
+ * Handles event-specific operations for Nostr protocol
+ */
+
+// External imports
+import * as nostrTools from 'nostr-tools';
+
+// Project imports
+import type { NostrEvent } from '../relay';
+
+import type { NostrCacheService, ProfileData } from './NostrCacheService';
+import type { NostrRelayService } from './NostrRelayService';
+
+// Interface for a Nostr subscription
+export interface NostrSubscription {
+  unsub: () => void;
+}
+
+// Interface for a received event
+export interface ReceivedEvent {
+  id: string;
+  event: NostrEvent;
+  receivedAt: number;
+  decrypted: boolean;
+  decryptedContent?: string;
+}
+
+// Interface for Nostr filter
+export interface NostrFilter {
+  kinds: number[];
+  '#p'?: string[];
+  authors?: string[];
+  since?: number;
+  until?: number;
+  limit?: number;
+  ids?: string[];
+  [key: string]: unknown;
+}
+
+/**
+ * Class for managing Nostr event operations
+ */
+export class NostrEventService {
+  private relayService: NostrRelayService;
+  private cacheService: NostrCacheService;
+  private eventHandler: ((receivedEvent: NostrEvent) => void) | null = null;
+  private statusCallback: ((statusMessage: string, statusClass: string) => void) | null = null;
+
+  /**
+   * Constructor
+   */
+  constructor(
+    relayService: NostrRelayService,
+    cacheService: NostrCacheService,
+    statusCallback?: ((statusMessage: string, statusClass: string) => void)
+  ) {
+    this.relayService = relayService;
+    this.cacheService = cacheService;
+    this.statusCallback = statusCallback || null;
+  }
+
+  /**
+   * Set the event handler for received events
+   */
+  public setEventHandler(handler: ((receivedEvent: NostrEvent) => void)): void {
+    this.eventHandler = handler;
+  }
+
+  /**
+   * Subscribe to events with the given filter
+   */
+  public async subscribeToEvents(filter: NostrFilter): Promise<NostrSubscription> {
+    const activeRelayUrl = this.relayService.getActiveRelayUrl();
+    if (!activeRelayUrl) {
+      throw new Error('No active relay URL');
+    }
+    
+    this.updateStatus('Creating subscription...', 'connecting');
+    
+    try {
+      const wsManager = this.relayService.getWebSocketManager();
+      await wsManager.connect(activeRelayUrl, {
+        timeout: 5000,
+        onOpen: (ws) => {
+          // Send a REQ message to subscribe
+          const reqId = `req-${Date.now()}`;
+          const reqMsg = JSON.stringify(["REQ", reqId, filter]);
+          ws.send(reqMsg);
+          
+          this.updateStatus('Subscription active ✓', 'connected');
+        },
+        onMessage: (data) => {
+          // Type assertion for the received data
+          const nostrData = data as unknown[];
+          
+          // Handle different message types
+          if (Array.isArray(nostrData) && nostrData[0] === "EVENT" && nostrData.length >= 3) {
+            const receivedEvent = nostrData[2] as NostrEvent;
+            
+            // Process the event if we have a handler
+            if (this.eventHandler && receivedEvent.id) {
+              const callback = this.eventHandler;
+              callback(receivedEvent);
+            }
+          }
+        },
+        onError: () => {
+          this.updateStatus('WebSocket error', 'error');
+        },
+        onClose: () => {
+          this.updateStatus('Connection closed', 'error');
+        }
+      });
+      
+      // Return a subscription object
+      return {
+        unsub: () => {
+          wsManager.close();
+        }
+      };
+    } catch (error) {
+      this.updateStatus(
+        `Subscription error: ${error instanceof Error ? error.message : String(error)}`, 
+        'error'
+      );
+      throw error;
+    }
+  }
+
+  /**
+   * Create a filter for kind 21120 events
+   */
+  public createKind21120Filter(showAllEvents: boolean): NostrFilter {
+    // Create filter for kind 21120 events
+    const filter: NostrFilter = {
+      kinds: [21120], // HTTP Messages event kind
+    };
+    
+    // If "Show all events" is not checked, filter only for events addressed to the server
+    if (!showAllEvents) {
+      // Get the server pubkey from localStorage (set during server registration)
+      const serverNsec = localStorage.getItem('serverNsec');
+      let serverPubkey = null;
+      
+      if (serverNsec) {
+        try {
+          // Decode the nsec to get the private key
+          const decoded = nostrTools.nip19.decode(serverNsec as string);
+          if (decoded.type === 'nsec') {
+            // Get the server pubkey from the private key
+            const privateKeyBytes = decoded.data as Uint8Array;
+            serverPubkey = nostrTools.getPublicKey(privateKeyBytes);
+          }
+        } catch (error) {
+          console.error('Error getting server pubkey:', error);
+        }
+      }
+      
+      // Add p-tag filter for events addressed to the server
+      if (serverPubkey) {
+        filter['#p'] = [serverPubkey];
+      }
+    }
+    
+    return filter;
+  }
+
+  /**
+   * Fetch profile data for a pubkey
+   */
+  public async fetchProfileData(pubkey: string): Promise<ProfileData | null> {
+    // Return from cache if available
+    const cachedProfile = this.cacheService.getCachedProfile(pubkey);
+    if (cachedProfile) {
+      return cachedProfile;
+    }
+    
+    // Placeholder for actual implementation
+    return null;
+  }
+
+  /**
+   * Query for a specific 31120 event
+   */
+  public async queryFor31120Event(relayUrl: string, authorPubkey?: string | null): Promise<NostrEvent | null> {
+    console.log(`Querying for kind 31120 event by author: ${authorPubkey?.substring(0, 8) || 'any'}`);
+    
+    try {
+      // Connect to relay if needed
+      if (!this.relayService.isConnected() || this.relayService.getActiveRelayUrl() !== relayUrl) {
+        await this.relayService.connectToRelay(relayUrl);
+      }
+      
+      // Check the cache first
+      const cachedEvents = this.cacheService.getCachedEvents(relayUrl);
+      if (cachedEvents && cachedEvents.length > 0) {
+        // Filter cached events for kind 31120 and the author if specified
+        const filteredEvents = cachedEvents.filter(event => {
+          if (event.kind !== 31120) {
+            return false;
+          }
+          if (authorPubkey && event.pubkey !== authorPubkey) {
+            return false;
+          }
+          return true;
+        });
+        
+        if (filteredEvents.length > 0) {
+          console.log(`Found ${filteredEvents.length} cached 31120 events`);
+          // Sort events by created_at (newest first) and return the first one
+          const sortedEvents = [...filteredEvents].sort((a, b) => b.created_at - a.created_at);
+          return sortedEvents[0];
+        }
+      }
+      
+      // If we reach here, no events found in cache
+      return null;
+    } catch (error) {
+      console.error("Error querying for 31120 event:", error);
+      return null;
+    }
+  }
+
+  /**
+   * Update the status via callback if set
+   */
+  private updateStatus(statusMessage: string, statusClass: string): void {
+    if (this.statusCallback) {
+      const callback = this.statusCallback;
+      callback(statusMessage, statusClass);
+    }
+  }
+}
diff --git a/client/src/services/NostrEventService.ts.new b/client/src/services/NostrEventService.ts.new
new file mode 100644
index 0000000..93e4dbc
--- /dev/null
+++ b/client/src/services/NostrEventService.ts.new
@@ -0,0 +1,394 @@
+/**
+ * NostrEventService.ts
+ * Handles event-specific operations for Nostr protocol
+ */
+
+// External imports
+import * as nostrTools from 'nostr-tools';
+
+// Project imports
+import type { NostrEvent } from '../relay';
+
+import type { NostrCacheService, ProfileData } from './NostrCacheService';
+import type { NostrRelayService } from './NostrRelayService';
+
+// Interface for a Nostr subscription
+export interface NostrSubscription {
+  unsub: () => void;
+}
+
+// Interface for a received event
+export interface ReceivedEvent {
+  id: string;
+  event: NostrEvent;
+  receivedAt: number;
+  decrypted: boolean;
+  decryptedContent?: string;
+}
+
+// Interface for Nostr filter
+export interface NostrFilter {
+  kinds: number[];
+  '#p'?: string[];
+  authors?: string[];
+  since?: number;
+  until?: number;
+  limit?: number;
+  ids?: string[];
+  [key: string]: unknown;
+}
+
+/**
+ * Class for managing Nostr event operations
+ */
+export class NostrEventService {
+  private relayService: NostrRelayService;
+  private cacheService: NostrCacheService;
+  private eventHandler: ((event: NostrEvent) => void) | null = null;
+  private statusCallback: ((message: string, className: string) => void) | null = null;
+
+  /**
+   * Constructor
+   */
+  constructor(
+    relayService: NostrRelayService,
+    cacheService: NostrCacheService,
+    statusCallback?: ((message: string, className: string) => void)
+  ) {
+    this.relayService = relayService;
+    this.cacheService = cacheService;
+    this.statusCallback = statusCallback || null;
+  }
+
+  /**
+   * Set the event handler for received events
+   */
+  public setEventHandler(handler: ((event: NostrEvent) => void)): void {
+    this.eventHandler = handler;
+  }
+
+  /**
+   * Subscribe to events with the given filter
+   */
+  public async subscribeToEvents(filter: NostrFilter): Promise<NostrSubscription> {
+    const activeRelayUrl = this.relayService.getActiveRelayUrl();
+    if (!activeRelayUrl) {
+      throw new Error('No active relay URL');
+    }
+    
+    this.updateStatus('Creating subscription...', 'connecting');
+    
+    try {
+      const wsManager = this.relayService.getWebSocketManager();
+      await wsManager.connect(activeRelayUrl, {
+        timeout: 5000,
+        onOpen: (ws) => {
+          // Send a REQ message to subscribe
+          const reqId = `req-${Date.now()}`;
+          const reqMsg = JSON.stringify(["REQ", reqId, filter]);
+          ws.send(reqMsg);
+          
+          this.updateStatus('Subscription active ✓', 'connected');
+        },
+        onMessage: (data) => {
+          // Type assertion for the received data
+          const nostrData = data as unknown[];
+          
+          // Handle different message types
+          if (Array.isArray(nostrData) && nostrData[0] === "EVENT" && nostrData.length >= 3) {
+            const receivedEvent = nostrData[2] as NostrEvent;
+            
+            // Process the event if we have a handler
+            if (this.eventHandler && receivedEvent.id) {
+              const callback = this.eventHandler;
+              callback(receivedEvent);
+            }
+          }
+        },
+        onError: () => {
+          this.updateStatus('WebSocket error', 'error');
+        },
+        onClose: () => {
+          this.updateStatus('Connection closed', 'error');
+        }
+      });
+      
+      // Return a subscription object
+      return {
+        unsub: () => {
+          wsManager.close();
+        }
+      };
+    } catch (error) {
+      this.updateStatus(
+        `Subscription error: ${error instanceof Error ? error.message : String(error)}`, 
+        'error'
+      );
+      throw error;
+    }
+  }
+
+  /**
+   * Create a filter for kind 21120 events
+   */
+  public createKind21120Filter(showAllEvents: boolean): NostrFilter {
+    // Create filter for kind 21120 events
+    const filter: NostrFilter = {
+      kinds: [21120], // HTTP Messages event kind
+    };
+    
+    // If "Show all events" is not checked, filter only for events addressed to the server
+    if (!showAllEvents) {
+      // Get the server pubkey from localStorage (set during server registration)
+      const serverNsec = localStorage.getItem('serverNsec');
+      let serverPubkey = null;
+      
+      if (serverNsec) {
+        try {
+          // Decode the nsec to get the private key
+          const decoded = nostrTools.nip19.decode(serverNsec as string);
+          if (decoded.type === 'nsec') {
+            // Get the server pubkey from the private key
+            const privateKeyBytes = decoded.data as Uint8Array;
+            serverPubkey = nostrTools.getPublicKey(privateKeyBytes);
+          }
+        } catch (error) {
+          // eslint-disable-next-line no-console
+          console.error('Error getting server pubkey:', error);
+        }
+      }
+      
+      // Add p-tag filter for events addressed to the server
+      if (serverPubkey) {
+        filter['#p'] = [serverPubkey];
+      }
+    }
+    
+    return filter;
+  }
+
+  /**
+   * Fetch profile data for a pubkey
+   */
+  public async fetchProfileData(pubkey: string): Promise<ProfileData | null> {
+    // Return from cache if available
+    const cachedProfile = this.cacheService.getCachedProfile(pubkey);
+    if (cachedProfile) {
+      return cachedProfile;
+    }
+    
+    // Placeholder for actual implementation
+    return null;
+  }
+
+  /**
+   * Query for a specific 31120 event
+   */
+  public async queryFor31120Event(relayUrl: string, authorPubkey?: string | null): Promise<NostrEvent | null> {
+    console.log(`Querying for kind 31120 event by author: ${authorPubkey?.substring(0, 8) || 'any'}`);
+    
+    try {
+      // Connect to relay if needed
+      if (!this.relayService.isConnected() || this.relayService.getActiveRelayUrl() !== relayUrl) {
+        await this.relayService.connectToRelay(relayUrl);
+      }
+      
+      // Check the cache first
+      const cachedEvents = this.cacheService.getCachedEvents(relayUrl);
+      if (cachedEvents && cachedEvents.length > 0) {
+        // Filter cached events for kind 31120 and the author if specified
+        const filteredEvents = cachedEvents.filter(event => {
+          if (event.kind !== 31120) return false;
+          if (authorPubkey && event.pubkey !== authorPubkey) return false;
+          return true;
+        });
+        
+        if (filteredEvents.length > 0) {
+          console.log(`Found ${filteredEvents.length} cached 31120 events`);
+          // Sort events by created_at (newest first) and return the first one
+          const sortedEvents = [...filteredEvents].sort((a, b) => b.created_at - a.created_at);
+          return sortedEvents[0];
+        }
+      }
+      
+      // Create filter for kind 31120 events
+      const filter: NostrFilter = {
+        kinds: [31120],
+        limit: 10
+      };
+      
+      // Add author filter if provided
+      if (authorPubkey) {
+        filter.authors = [authorPubkey];
+      }
+      
+      // Set a timeout for the query
+      const timeoutMs = 5000;
+      const timeoutPromise = new Promise<NostrEvent[]>((_, reject) => {
+        setTimeout(() => reject(new Error(`Query timed out after ${timeoutMs}ms`)), timeoutMs);
+      });
+      
+      // Create a promise that will resolve with the query results
+      const queryPromise = new Promise<NostrEvent[]>((resolve) => {
+        const events: NostrEvent[] = [];
+        const reqId = `req-${Date.now()}`;
+        
+        // Create a new connection with custom message handler
+        (async () => {
+          try {
+            // Ensure we're connected to the relay
+            const connected = await this.relayService.connectToRelay(relayUrl);
+            if (!connected) {
+              resolve([]);
+              return;
+            }
+            
+            // Get the WebSocket manager
+            const wsManager = this.relayService.getWebSocketManager();
+            
+            // Connect with custom message handler
+            await wsManager.connect(relayUrl, {
+              onMessage: (data: unknown) => {
+                try {
+                  const message = data as unknown[];
+                  if (Array.isArray(message) && message[0] === "EVENT" && message[1] === reqId) {
+                    // Add the event to our results
+                    const event = message[2] as NostrEvent;
+                    events.push(event);
+                  } else if (Array.isArray(message) && message[0] === "EOSE" && message[1] === reqId) {
+                    // End of stored events, resolve the promise
+                    resolve(events);
+                  }
+                } catch (error) {
+                  console.error("Error processing message:", error);
+                }
+              }
+            });
+            
+            // Send the query to the relay
+            const reqMsg = JSON.stringify(["REQ", reqId, filter]);
+            wsManager.send(reqMsg);
+          } catch (error) {
+            console.error("Error setting up event query:", error);
+            resolve([]);
+          }
+        })();
+      });
+      
+      // Execute the query with a timeout
+      const events = await Promise.race([queryPromise, timeoutPromise]);
+      
+      // Cache the results
+      if (events.length > 0) {
+        this.cacheService.cacheEvents(relayUrl, events);
+      }
+      
+      console.log(`Found ${events.length} 31120 events from relay`);
+      
+      // Return null if no events were found, otherwise the most recent event
+      if (events.length === 0) {
+        return null;
+      }
+      
+      // Sort events by created_at (newest first) and return the first one
+      const sortedEvents = [...events].sort((a, b) => b.created_at - a.created_at);
+      return sortedEvents[0];
+    } catch (error) {
+      console.error("Error querying for 31120 event:", error);
+      return null;
+    }
+  }
+
+  /**
+   * Query for all 31120 events
+   */
+  public async queryForAll31120Events(relayUrl: string): Promise<NostrEvent[]> {
+    console.log(`Querying for all kind 31120 events from ${relayUrl}`);
+    
+    try {
+      // Connect to relay if needed
+      if (!this.relayService.isConnected() || this.relayService.getActiveRelayUrl() !== relayUrl) {
+        await this.relayService.connectToRelay(relayUrl);
+      }
+      
+      // Check the cache first
+      const cachedEvents = this.cacheService.getCachedEvents(relayUrl);
+      if (cachedEvents && cachedEvents.length > 0) {
+        // Filter cached events for kind 31120
+        const filteredEvents = cachedEvents.filter(event => event.kind === 31120);
+        if (filteredEvents.length > 0) {
+          console.log(`Using ${filteredEvents.length} cached 31120 events`);
+          return filteredEvents;
+        }
+      }
+      
+      // Create filter for all kind 31120 events
+      const filter: NostrFilter = {
+        kinds: [31120],
+        limit: 100  // Reasonable limit for server advertisements
+      };
+      
+      // Set a timeout for the query
+      const timeoutMs = 8000;
+      const timeoutPromise = new Promise<NostrEvent[]>((_, reject) => {
+        setTimeout(() => reject(new Error(`Query timed out after ${timeoutMs}ms`)), timeoutMs);
+      });
+      
+      // Create a promise that will resolve with the query results
+      const queryPromise = new Promise<NostrEvent[]>((resolve) => {
+        const events: NostrEvent[] = [];
+        const reqId = `req-${Date.now()}`;
+        
+        // Create a new connection with custom message handler
+        (async () => {
+          try {
+            // Ensure we're connected to the relay
+            const connected = await this.relayService.connectToRelay(relayUrl);
+            if (!connected) {
+              resolve([]);
+              return;
+            }
+            
+            // Get the WebSocket manager
+            const wsManager = this.relayService.getWebSocketManager();
+            
+            // Connect with custom message handler
+            await wsManager.connect(relayUrl, {
+              onMessage: (data: unknown) => {
+                try {
+                  const message = data as unknown[];
+                  if (Array.isArray(message) && message[0] === "EVENT" && message[1] === reqId) {
+                    // Add the event to our results
+                    const event = message[2] as NostrEvent;
+                    events.push(event);
+                  } else if (Array.isArray(message) && message[0] === "EOSE" && message[1] === reqId) {
+                    // End of stored events, resolve the promise
+                    resolve(events);
+                  }
+                } catch (error) {
+                  console.error("Error processing message:", error);
+                }
+              }
+            });
+            
+            // Send the query to the relay
+            const reqMsg = JSON.stringify(["REQ", reqId, filter]);
+            wsManager.send(reqMsg);
+          } catch (error) {
+            console.error("Error setting up event query:", error);
+            resolve([]);
+          }
+        })();
+      });
+      
+      // Execute the query with a timeout
+      let events: NostrEvent[];
+      try {
+        events = await Promise.race([queryPromise, timeoutPromise]);
+      } catch (error) {
+        console.error("Query timed out or failed:", error);
+        // Return cached events if any
+        return cachedEvents?.filter(event => event.kind === 31120) || [];
+      }
+      
+      // Filter out expired events
diff --git a/client/src/services/NostrRelayService.ts b/client/src/services/NostrRelayService.ts
new file mode 100644
index 0000000..7889dca
--- /dev/null
+++ b/client/src/services/NostrRelayService.ts
@@ -0,0 +1,115 @@
+/**
+ * NostrRelayService.ts
+ * Handles relay connection management for Nostr protocol
+ */
+
+// External imports
+import * as nostrTools from 'nostr-tools';
+
+// Project imports
+import { WebSocketManager } from './WebSocketManager';
+
+/**
+ * Class for managing Nostr relay connections
+ */
+export class NostrRelayService {
+  private relayPool: nostrTools.SimplePool | null = null;
+  private wsManager = new WebSocketManager();
+  private activeRelayUrl: string | null = null;
+  // eslint-disable-next-line no-unused-vars
+  private statusCallback: ((message: string, className: string) => void) | null = null;
+
+  /**
+   * Constructor
+   * @param statusCallback Callback for status updates
+   */
+  // eslint-disable-next-line no-unused-vars
+  constructor(statusCallback?: ((message: string, className: string) => void)) {
+    this.statusCallback = statusCallback || null;
+  }
+
+  /**
+   * Connect to a relay
+   * @param relayUrl The relay URL to connect to
+   * @returns A promise that resolves to true if connected successfully
+   */
+  public async connectToRelay(relayUrl: string): Promise<boolean> {
+    try {
+      this.updateStatus('Connecting to relay...', 'connecting');
+      
+      // Test the connection first
+      const connectionSuccess = await this.wsManager.testConnection(relayUrl);
+      if (!connectionSuccess) {
+        this.updateStatus('Connection failed', 'error');
+        return false;
+      }
+      
+      // Close existing relay pool if any
+      if (this.relayPool && this.activeRelayUrl) {
+        try {
+          await this.relayPool.close([this.activeRelayUrl]);
+        } catch {
+          // Ignore errors when closing
+        }
+        this.relayPool = null;
+      }
+      
+      // Create new relay pool
+      this.relayPool = new nostrTools.SimplePool();
+      this.activeRelayUrl = relayUrl;
+      
+      this.updateStatus('Connected', 'connected');
+      return true;
+    } catch (error) {
+      this.updateStatus(
+        `Error: ${error instanceof Error ? error.message : String(error)}`, 
+        'error'
+      );
+      return false;
+    }
+  }
+
+  /**
+   * Check if the service is connected to a relay
+   * @returns true if connected, false otherwise
+   */
+  public isConnected(): boolean {
+    return this.relayPool !== null && this.activeRelayUrl !== null && this.wsManager.isConnected();
+  }
+
+  /**
+   * Get the WebSocket manager instance
+   * @returns The WebSocket manager
+   */
+  public getWebSocketManager(): WebSocketManager {
+    return this.wsManager;
+  }
+
+  /**
+   * Get the relay pool instance
+   * @returns The relay pool
+   */
+  public getRelayPool(): nostrTools.SimplePool | null {
+    return this.relayPool;
+  }
+
+  /**
+   * Get the active relay URL
+   * @returns The active relay URL
+   */
+  public getActiveRelayUrl(): string | null {
+    return this.activeRelayUrl;
+  }
+
+  /**
+   * Update the status via callback if set
+   * @param message The status message
+   * @param className The CSS class name for styling
+   */
+  private updateStatus(message: string, className: string): void {
+    if (this.statusCallback) {
+      const callback = this.statusCallback;
+      callback(message, className);
+    }
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/NostrService.ts b/client/src/services/NostrService.ts
index e5991ef..abe6985 100644
--- a/client/src/services/NostrService.ts
+++ b/client/src/services/NostrService.ts
@@ -1,127 +1,86 @@
 /**
  * NostrService.ts
- * Handles Nostr-specific functionality like events, subscriptions, and filtering
+ * Main service that coordinates Nostr protocol functionality by integrating specialized services
  */
 
-// External imports
-import * as nostrTools from 'nostr-tools';
-
 // Project imports
 import type { NostrEvent } from '../relay';
-import { convertNpubToHex, publishToRelay } from '../relay';
 
-import { WebSocketManager } from './WebSocketManager';
-
-// Interface for profile data
-export interface ProfileData {
-  name?: string;
-  about?: string;
-  picture?: string;
-  nip05?: string;
-  [key: string]: unknown;
-}
-
-// Interface for a Nostr subscription
-export interface NostrSubscription {
-  unsub: () => void;
-}
-
-// Interface for a received event
-export interface ReceivedEvent {
-  id: string;
-  event: NostrEvent;
-  receivedAt: number;
-  decrypted: boolean;
-  decryptedContent?: string;
-}
-
-// Interface for Nostr filter
-export interface NostrFilter {
-  kinds: number[];
-  '#p'?: string[];
-  authors?: string[];
-  since?: number;
-  until?: number;
-  limit?: number;
-  [key: string]: unknown;
-}
+import type { ProfileData } from './NostrCacheService';
+import { NostrCacheService } from './NostrCacheService';
+import type { NostrFilter, NostrSubscription } from './NostrEventService';
+import { NostrEventService } from './NostrEventService';
+import { NostrRelayService } from './NostrRelayService';
+import { Nostr31120Service } from './Nostr31120Service';
+import { Nostr21121Service } from './Nostr21121Service';
 
 /**
  * Class for managing Nostr functionality
+ * Acts as a facade for specialized services
  */
 export class NostrService {
-  private profileCache = new Map<string, ProfileData>();
-  private relayPool: nostrTools.SimplePool | null = null;
-  private wsManager = new WebSocketManager();
-  private activeRelayUrl: string | null = null;
-  // Local memory cache (for immediate access during the session)
-  private events31120Cache = new Map<string, [number, {[key: string]: NostrEvent}]>();
-  // Cache expiration time in milliseconds (5 minutes)
-  private cacheExpiryTime = 5 * 60 * 1000;
-  // localStorage key prefix
-  private localStorageCachePrefix = 'nostr_31120_cache_';
-  // eslint-disable-next-line no-unused-vars
-  private eventHandler: ((event: NostrEvent) => void) | null = null;
-  // eslint-disable-next-line no-unused-vars
-  private statusCallback: ((message: string, className: string) => void) | null = null;
-
+  private relayService: NostrRelayService;
+  private cacheService: NostrCacheService;
+  private eventService: NostrEventService;
+  private service31120: Nostr31120Service;
+  private service21121: Nostr21121Service;
+  
+  /**
+   * Get the relay service instance
+   */
+  public getRelayService(): NostrRelayService {
+    return this.relayService;
+  }
+  
+  /**
+   * Get the cache service instance
+   */
+  public getCacheService(): NostrCacheService {
+    return this.cacheService;
+  }
+  
   /**
    * Constructor
    * @param statusCallback Callback for status updates
    */
   // eslint-disable-next-line no-unused-vars
   constructor(statusCallback?: ((message: string, className: string) => void)) {
-    this.statusCallback = statusCallback || null;
-  }
-
-  /**
-   * Set the event handler for received events
-   * @param handler The handler function for received events
-   */
-  // eslint-disable-next-line no-unused-vars
-  public setEventHandler(handler: ((event: NostrEvent) => void)): void {
-    this.eventHandler = handler;
+    this.relayService = new NostrRelayService(statusCallback);
+    this.cacheService = new NostrCacheService();
+    this.eventService = new NostrEventService(this.relayService, this.cacheService, statusCallback);
+    // Initialize the dedicated services for events
+    this.service31120 = new Nostr31120Service(this.relayService, this.cacheService);
+    this.service21121 = new Nostr21121Service(this.relayService, this.cacheService);
   }
 
+  // Connection methods delegated to NostrRelayService
+  
   /**
    * Connect to a relay
    * @param relayUrl The relay URL to connect to
    * @returns A promise that resolves to true if connected successfully
    */
   public async connectToRelay(relayUrl: string): Promise<boolean> {
-    try {
-      this.updateStatus('Connecting to relay...', 'connecting');
-      
-      // Test the connection first
-      const connectionSuccess = await this.wsManager.testConnection(relayUrl);
-      if (!connectionSuccess) {
-        this.updateStatus('Connection failed', 'error');
-        return false;
-      }
-      
-      // Close existing relay pool if any
-      if (this.relayPool && this.activeRelayUrl) {
-        try {
-          await this.relayPool.close([this.activeRelayUrl]);
-        } catch {
-          // Ignore errors when closing
-        }
-        this.relayPool = null;
-      }
-      
-      // Create new relay pool
-      this.relayPool = new nostrTools.SimplePool();
-      this.activeRelayUrl = relayUrl;
-      
-      this.updateStatus('Connected', 'connected');
-      return true;
-    } catch (error) {
-      this.updateStatus(
-        `Error: ${error instanceof Error ? error.message : String(error)}`, 
-        'error'
-      );
-      return false;
-    }
+    return this.relayService.connectToRelay(relayUrl);
+  }
+
+  /**
+   * Check if the service is connected to a relay
+   * @returns true if connected, false otherwise
+   */
+  public isConnected(): boolean {
+    return this.relayService.isConnected();
+  }
+
+  // Event-specific methods delegated to NostrEventService
+  
+  /**
+   * Set the event handler for received events
+   * @param handler The handler function for received events
+   */
+  // eslint-disable-next-line no-unused-vars
+  public setEventHandler(handler: ((event: NostrEvent) => void)): void {
+    this.eventService.setEventHandler(handler);
   }
 
   /**
@@ -130,67 +89,7 @@ export class NostrService {
    * @returns A promise that resolves to a NostrSubscription
    */
   public async subscribeToEvents(filter: NostrFilter): Promise<NostrSubscription> {
-    if (!this.activeRelayUrl) {
-      throw new Error('No active relay URL');
-    }
-    
-    this.updateStatus('Creating subscription...', 'connecting');
-    
-    try {
-      await this.wsManager.connect(this.activeRelayUrl, {
-        timeout: 5000,
-        onOpen: (ws) => {
-          // Send a REQ message to subscribe
-          const reqId = `req-${Date.now()}`;
-          const reqMsg = JSON.stringify(["REQ", reqId, filter]);
-          ws.send(reqMsg);
-          
-          this.updateStatus('Subscription active ✓', 'connected');
-        },
-        onMessage: (data) => {
-          // Type assertion for the received data
-          const nostrData = data as unknown[];
-          
-          // Handle different message types
-          if (Array.isArray(nostrData) && nostrData[0] === "EVENT" && nostrData.length >= 3) {
-            const event = nostrData[2] as NostrEvent;
-            
-            // Process the event if we have a handler
-            if (this.eventHandler && event.id) {
-              const callback = this.eventHandler;
-              callback(event);
-            }
-          }
-        },
-        onError: () => {
-          this.updateStatus('WebSocket error', 'error');
-        },
-        onClose: () => {
-          this.updateStatus('Connection closed', 'error');
-        }
-      });
-      
-      // Return a subscription object
-      return {
-        unsub: () => {
-          this.wsManager.close();
-        }
-      };
-    } catch (error) {
-      this.updateStatus(
-        `Subscription error: ${error instanceof Error ? error.message : String(error)}`, 
-        'error'
-      );
-      throw error;
-    }
-  }
-
-  /**
-   * Check if the service is connected to a relay
-   * @returns true if connected, false otherwise
-   */
-  public isConnected(): boolean {
-    return this.relayPool !== null && this.activeRelayUrl !== null && this.wsManager.isConnected();
+    return this.eventService.subscribeToEvents(filter);
   }
 
   /**
@@ -200,105 +99,7 @@ export class NostrService {
    * @returns Promise resolving to the found event or null if not found
    */
   public async queryFor31120Event(relayUrl: string, authorPubkey?: string | null): Promise<NostrEvent | null> {
-    console.log('Querying for 31120 event...');
-    
-    // Prepare filter for kind 31120 events
-    const filter: any = {
-      kinds: [31120]
-    };
-    
-    // If authorPubkey is provided, add it to the filter
-    if (authorPubkey) {
-      // Convert to hex if needed
-      let pubkeyHex = authorPubkey;
-      if (authorPubkey.startsWith('npub')) {
-        try {
-          const hexPubkey = convertNpubToHex(authorPubkey);
-          if (hexPubkey) {
-            pubkeyHex = hexPubkey;
-          }
-        } catch (error) {
-          console.error('Error converting npub to hex:', error);
-          return null;
-        }
-      }
-      
-      // Add author filter
-      filter.authors = [pubkeyHex];
-    }
-
-    try {
-      // Connect to relay if not already connected
-      if (!this.isConnected() || this.activeRelayUrl !== relayUrl) {
-        const connected = await this.connectToRelay(relayUrl);
-        if (!connected) {
-          throw new Error(`Failed to connect to relay: ${relayUrl}`);
-        }
-      }
-
-      // Query for events
-      this.updateStatus('Querying for 31120 events...', 'connecting');
-
-      // Use a promise to wait for the query result
-      return new Promise((resolve, reject) => {
-        const requestId = `query-31120-${Date.now()}`;
-        const wsTimeout = setTimeout(() => {
-          this.wsManager.close();
-          reject(new Error('Query timeout'));
-        }, 10000);
-
-        this.wsManager.connect(relayUrl, {
-          timeout: 5000,
-          onOpen: (ws) => {
-            // Send a REQ message to query
-            const reqMsg = JSON.stringify(["REQ", requestId, filter]);
-            ws.send(reqMsg);
-            this.updateStatus('Querying relay...', 'connecting');
-          },
-          onMessage: (data) => {
-            // Parse the message
-            const nostrData = data as unknown[];
-            
-            // Check if it's an EVENT message
-            if (Array.isArray(nostrData) && nostrData[0] === "EVENT" && nostrData[1] === requestId) {
-              const event = nostrData[2] as NostrEvent;
-              
-              // Check if this is a 31120 event
-              if (event.kind === 31120) {
-                // If we're looking for specific author, check it matches
-                if (authorPubkey && filter.authors && event.pubkey !== filter.authors[0]) {
-                  return; // Continue looking for events with matching pubkey
-                }
-                
-                clearTimeout(wsTimeout);
-                this.wsManager.close();
-                resolve(event);
-              }
-            }
-            
-            // If it's an EOSE message, we've received all events
-            if (Array.isArray(nostrData) && nostrData[0] === "EOSE" && nostrData[1] === requestId) {
-              clearTimeout(wsTimeout);
-              this.wsManager.close();
-              resolve(null); // No matching event found
-            }
-          },
-          onError: (error) => {
-            clearTimeout(wsTimeout);
-            reject(new Error(`WebSocket error: ${error}`));
-          },
-          onClose: () => {
-            clearTimeout(wsTimeout);
-          }
-        }).catch(error => {
-          clearTimeout(wsTimeout);
-          reject(error);
-        });
-      });
-    } catch (error) {
-      this.updateStatus(`Query error: ${error instanceof Error ? error.message : String(error)}`, 'error');
-      return null;
-    }
+    return this.eventService.queryFor31120Event(relayUrl, authorPubkey);
   }
 
   /**
@@ -307,243 +108,8 @@ export class NostrService {
    * @returns Promise resolving to an array of matching events
    */
   public async queryForAll31120Events(relayUrl: string): Promise<NostrEvent[]> {
-    console.log('Querying for all 31120 events...');
-    
-    // First, try to get from in-memory cache (fastest)
-    const cachedData = this.events31120Cache.get(relayUrl);
-    const now = Date.now();
-    
-    if (cachedData) {
-      const [timestamp, eventsMap] = cachedData;
-      
-      // If memory cache is still valid (less than cacheExpiryTime milliseconds old)
-      if (now - timestamp < this.cacheExpiryTime) {
-        console.log(`Using in-memory cached 31120 events for ${relayUrl} (${Object.keys(eventsMap).length} events)`);
-        return Object.values(eventsMap);
-      }
-    }
-    
-    // Second, try localStorage (persists between page navigations)
-    const localStorageKey = this.localStorageCachePrefix + relayUrl;
-    const storedCache = localStorage.getItem(localStorageKey);
-    
-    if (storedCache) {
-      try {
-        const parsedCache = JSON.parse(storedCache) as [number, {[key: string]: NostrEvent}];
-        const [timestamp, eventsMap] = parsedCache;
-        
-        // If localStorage cache is still valid
-        if (now - timestamp < this.cacheExpiryTime) {
-          console.log(`Using localStorage cached 31120 events for ${relayUrl} (${Object.keys(eventsMap).length} events)`);
-          
-          // Update in-memory cache too
-          this.events31120Cache.set(relayUrl, parsedCache);
-          
-          return Object.values(eventsMap);
-        }
-        console.log(`localStorage cache expired for ${relayUrl}, fetching fresh data...`);
-      } catch (error) {
-        console.error('Error parsing localStorage cache:', error);
-        // Continue to fetch from network
-      }
-    } else {
-      console.log(`No localStorage cache available for ${relayUrl}, fetching data...`);
-    }
-    
-    try {
-      // Connect to relay if not already connected
-      if (!this.isConnected() || this.activeRelayUrl !== relayUrl) {
-        const connected = await this.connectToRelay(relayUrl);
-        if (!connected) {
-          throw new Error(`Failed to connect to relay: ${relayUrl}`);
-        }
-      }
-
-      // Prepare filter for kind 31120 events
-      const filter = {
-        kinds: [31120]
-      };
-      
-      // Query for events
-      this.updateStatus('Querying for all 31120 events...', 'connecting');
-
-      // Use a promise to wait for all query results
-      return new Promise((resolve, reject) => {
-        const collectedEvents: NostrEvent[] = [];
-        const requestId = `query-all-31120-${Date.now()}`;
-        const wsTimeout = setTimeout(() => {
-          this.wsManager.close();
-          if (collectedEvents.length > 0) {
-            // Store in cache before resolving
-            this.cacheEvents(relayUrl, collectedEvents);
-            resolve(collectedEvents); // Return whatever we have if we hit timeout
-          } else {
-            reject(new Error('Query timeout'));
-          }
-        }, 10000);
-
-        this.wsManager.connect(relayUrl, {
-          timeout: 5000,
-          onOpen: (ws) => {
-            // Send a REQ message to query
-            const reqMsg = JSON.stringify(["REQ", requestId, filter]);
-            ws.send(reqMsg);
-            this.updateStatus('Querying relay for all servers...', 'connecting');
-          },
-          onMessage: (data) => {
-            // Parse the message
-            const nostrData = data as unknown[];
-            
-            // Check if it's an EVENT message
-            if (Array.isArray(nostrData) && nostrData[0] === "EVENT" && nostrData[1] === requestId) {
-              const event = nostrData[2] as NostrEvent;
-              
-              // Check if this is a 31120 event
-              if (event.kind === 31120) {
-                console.log('Found 31120 event:', event.id);
-                collectedEvents.push(event);
-              }
-            }
-            
-            // If it's an EOSE message, we've received all events
-            if (Array.isArray(nostrData) && nostrData[0] === "EOSE" && nostrData[1] === requestId) {
-              clearTimeout(wsTimeout);
-              this.wsManager.close();
-              console.log(`Found ${collectedEvents.length} 31120 events`);
-              
-              // Cache the events before resolving
-              this.cacheEvents(relayUrl, collectedEvents);
-              
-              resolve(collectedEvents);
-            }
-          },
-          onError: (error) => {
-            clearTimeout(wsTimeout);
-            reject(new Error(`WebSocket error: ${error}`));
-          },
-          onClose: () => {
-            clearTimeout(wsTimeout);
-          }
-        }).catch(error => {
-          clearTimeout(wsTimeout);
-          reject(error);
-        });
-      });
-    } catch (error) {
-      this.updateStatus(`Query error: ${error instanceof Error ? error.message : String(error)}`, 'error');
-      return [];
-    }
-  }
-
-  /**
-   * Create or update a 31120 event
-   * @param relayUrl The relay URL to publish to
-   * @param content The content/description of the server
-   * @param relays Array of relay URLs to include as tags
-   * @param expiryHours Number of hours until expiry
-   * @param existingEventId Optional ID of an existing event to update
-   * @returns Promise resolving to the created/updated event
-   */
-  public async createOrUpdate31120Event(
-    relayUrl: string,
-    content: string,
-    relays: string[],
-    expiryHours: number,
-    existingEventId?: string
-  ): Promise<NostrEvent | null> {
-    console.log(`${existingEventId ? 'Updating' : 'Creating'} 31120 event...`);
-    
-    try {
-      // Get the user's pubkey
-      const pubkey = this.getLoggedInPubkey();
-      if (!pubkey) {
-        throw new Error('No user pubkey available, cannot create/update 31120 event');
-      }
-
-      // Convert to hex if needed
-      let pubkeyHex = pubkey;
-      if (pubkey.startsWith('npub')) {
-        try {
-          const hexPubkey = convertNpubToHex(pubkey);
-          if (hexPubkey) {
-            pubkeyHex = hexPubkey;
-          }
-        } catch (error) {
-          throw new Error(`Error converting npub to hex: ${error}`);
-        }
-      }
-
-      // Set expiry to current time + specified hours (in seconds)
-      const now = Math.floor(Date.now() / 1000);
-      const expiry = now + (expiryHours * 60 * 60);
-      
-      // For existing events, we'll need to get the d-tag value for the server pubkey
-      let serverPubkey: string;
-      
-      if (existingEventId) {
-        // Get the existing event
-        const existingEvent = await this.getEventById(relayUrl, existingEventId);
-        if (!existingEvent) {
-          throw new Error('Could not find the event to update');
-        }
-        
-        // Get the d tag value (server pubkey)
-        const dTag = existingEvent.tags.find(tag => tag[0] === 'd');
-        if (!dTag || dTag.length < 2) {
-          throw new Error('Server pubkey not found in existing event');
-        }
-        
-        serverPubkey = dTag[1];
-      } else {
-        // Generate a new server key pair for new events
-        const serverKeyPair = nostrTools.generateSecretKey();
-        serverPubkey = nostrTools.getPublicKey(serverKeyPair);
-        const serverNsec = nostrTools.nip19.nsecEncode(serverKeyPair);
-        
-        // Store the server nsec locally
-        localStorage.setItem('serverNsec', serverNsec);
-      }
-      
-      // Create relay tags
-      const relayTags = relays.map(url => ["relay", url]);
-      
-      // Create the 31120 event
-      const event: NostrEvent = {
-        kind: 31120,
-        pubkey: pubkeyHex,
-        created_at: now,
-        content: content || "HTTP-over-Nostr server",
-        tags: [
-          ["d", serverPubkey], // Server pubkey
-          ...relayTags,
-          ["expiry", expiry.toString()]
-        ]
-      };
-
-      // Sign the event using window.nostr
-      if (!window.nostr || typeof window.nostr.signEvent !== 'function') {
-        throw new Error('No Nostr extension available for signing');
-      }
-
-      try {
-        // Request signing from the extension
-        const signedEvent = await window.nostr.signEvent(event);
-        
-        // Publish the signed event
-        this.updateStatus(`Publishing ${existingEventId ? 'updated' : 'new'} 31120 event...`, 'connecting');
-        await publishToRelay(signedEvent, relayUrl);
-        
-        console.log('31120 event published successfully');
-        this.updateStatus(`Server ${existingEventId ? 'updated' : 'registered'} ✓`, 'connected');
-        
-        return signedEvent;
-      } catch (error) {
-        throw new Error(`Error signing or publishing event: ${error}`);
-      }
-    } catch (error) {
-      this.updateStatus(`Error with 31120 event: ${error instanceof Error ? error.message : String(error)}`, 'error');
-      return null;
-    }
+    // Use the dedicated 31120 service instead of the event service
+    return this.service31120.queryForAll31120Events(relayUrl);
   }
 
   /**
@@ -553,89 +119,112 @@ export class NostrService {
    * @returns Promise resolving to the event or null if not found
    */
   public async getEventById(relayUrl: string, eventId: string): Promise<NostrEvent | null> {
-    console.log(`Fetching event with ID: ${eventId}`);
-    
     try {
-      // Connect to relay if not already connected
-      if (!this.isConnected() || this.activeRelayUrl !== relayUrl) {
-        const connected = await this.connectToRelay(relayUrl);
+      // Ensure we have an active connection
+      if (!this.relayService.isConnected() || this.relayService.getActiveRelayUrl() !== relayUrl) {
+        const connected = await this.relayService.connectToRelay(relayUrl);
         if (!connected) {
-          throw new Error(`Failed to connect to relay: ${relayUrl}`);
+          throw new Error(`Could not connect to relay: ${relayUrl}`);
         }
       }
       
+      // Get the relay pool
+      const relayPool = this.relayService.getRelayPool();
+      if (!relayPool) {
+        throw new Error('Relay pool not available');
+      }
+      
       // Create a filter for the specific event ID
       const filter = {
+        kinds: [31120],
         ids: [eventId]
       };
       
-      // Query for the event
-      return new Promise((resolve, reject) => {
-        const requestId = `query-event-${Date.now()}`;
-        const wsTimeout = setTimeout(() => {
-          this.wsManager.close();
-          reject(new Error('Query timeout'));
-        }, 10000);
-        
-        this.wsManager.connect(relayUrl, {
-          timeout: 5000,
-          onOpen: (ws) => {
-            // Send a REQ message to query
-            const reqMsg = JSON.stringify(["REQ", requestId, filter]);
-            ws.send(reqMsg);
-          },
-          onMessage: (data) => {
-            const nostrData = data as unknown[];
-            
-            // Check if it's an EVENT message
-            if (Array.isArray(nostrData) && nostrData[0] === "EVENT" && nostrData[1] === requestId) {
-              const event = nostrData[2] as NostrEvent;
-              if (event.id === eventId) {
-                clearTimeout(wsTimeout);
-                this.wsManager.close();
-                resolve(event);
-              }
-            }
-            
-            // If it's an EOSE message, we've received all events
-            if (Array.isArray(nostrData) && nostrData[0] === "EOSE" && nostrData[1] === requestId) {
-              clearTimeout(wsTimeout);
-              this.wsManager.close();
-              resolve(null); // Event not found
-            }
-          },
-          onError: (error) => {
-            clearTimeout(wsTimeout);
-            reject(new Error(`WebSocket error: ${error}`));
-          },
-          onClose: () => {
-            clearTimeout(wsTimeout);
-          }
-        }).catch(error => {
-          clearTimeout(wsTimeout);
-          reject(error);
-        });
-      });
+      // Query for the event using the proper method
+      const events = await relayPool.querySync([relayUrl], filter as any);
+      
+      // Return the first matching event, or null if none found
+      return events.length > 0 ? events[0] : null;
     } catch (error) {
-      console.error('Error fetching event:', error);
+      console.error('Error getting event by ID:', error);
       return null;
     }
   }
   
+  /**
+   * Create or update a 31120 event
+   * @param relayUrl The relay URL to publish to
+   * @param content The content/description of the server
+   * @param relays Array of relay URLs to include as tags
+   * @param expiryHours Number of hours until expiry
+   * @param existingEventId Optional ID of an existing event to update
+   * @returns Promise resolving to the created/updated event and optional server nsec
+   */
+  public async createOrUpdate31120Event(
+    relayUrl: string,
+    content: string,
+    relays: string[],
+    expiryHours: number,
+    existingEventId?: string,
+    customServerPubkey?: string
+  ): Promise<{ event: NostrEvent; serverNsec?: string } | null> {
+    // Use the dedicated 31120 service for this operation
+    return this.service31120.createOrUpdate31120Event(
+      relayUrl,
+      content,
+      relays,
+      expiryHours,
+      existingEventId,
+      customServerPubkey
+    );
+  }
+
   /**
    * Create and publish a new 31120 event (legacy method)
    * @param relayUrl The relay URL to publish to
-   * @returns Promise resolving to the created event
+   * @returns Promise resolving to the created event and optional server nsec
    */
-  public async create31120Event(relayUrl: string): Promise<NostrEvent | null> {
-    // Use the new method with default values
+  public async create31120Event(relayUrl: string): Promise<{ event: NostrEvent; serverNsec?: string } | null> {
+    // Use the full createOrUpdate31120Event method with default values
     return this.createOrUpdate31120Event(
       relayUrl,
-      "HTTP-over-Nostr server",
+      'HTTP-over-Nostr server',
       [relayUrl],
       24 // Default expiry of 24 hours
     );
   }
+  
+  /**
+   * Create and publish a 21121 response event
+   * @param requestEvent The 21120 request event to respond to
+   * @param responseContent The HTTP response content
+   * @param privateKey The private key to sign the event with
+   * @param relayUrl The relay URL to publish to
+   * @returns Promise resolving to the created event
+   */
+  public async createAndPublish21121Event(
+    requestEvent: NostrEvent,
+    responseContent: string,
+    privateKey: Uint8Array | string,
+    relayUrl: string
+  ): Promise<NostrEvent | null> {
+    return this.service21121.createAndPublish21121Event(
+      requestEvent,
+      responseContent,
+      privateKey,
+      relayUrl
+    );
+  }
+  
+  /**
+   * Find a 21121 response event for a specific 21120 request
+   * @param requestEventId The ID of the 21120 request event
+   * @param relayUrl The relay URL to search
+   * @returns Promise resolving to the response event or null if not found
+   */
+  public async findResponseForRequest(requestEventId: string, relayUrl: string): Promise<NostrEvent | null> {
+    return this.service21121.findResponseForRequest(requestEventId, relayUrl);
+  }
 
   /**
    * Create a filter for kind 21120 events, optionally filtered for a specific server
@@ -643,67 +232,10 @@ export class NostrService {
    * @returns A NostrFilter for the subscription
    */
   public createKind21120Filter(showAllEvents: boolean): NostrFilter {
-    // Create filter for kind 21120 events
-    const filter: NostrFilter = {
-      kinds: [21120], // HTTP Messages event kind
-    };
-    
-    // If "Show all events" is not checked, filter only for events addressed to the server
-    if (!showAllEvents) {
-      // Get the server pubkey from localStorage (set during server registration)
-      const serverNsec = localStorage.getItem('serverNsec');
-      let serverPubkey = null;
-      
-      if (serverNsec) {
-        try {
-          // Decode the nsec to get the private key
-          const decoded = nostrTools.nip19.decode(serverNsec);
-          if (decoded.type === 'nsec') {
-            // Get the server pubkey from the private key
-            // Convert Uint8Array to hex string
-            const privateKeyHex = Array.from(decoded.data as Uint8Array)
-              .map(b => b.toString(16).padStart(2, '0'))
-              .join('');
-            const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
-            serverPubkey = nostrTools.getPublicKey(privateKeyBytes);
-          }
-        } catch (error) {
-          console.error('Error getting server pubkey:', error);
-        }
-      }
-      
-      // Add p-tag filter for events addressed to the server
-      if (serverPubkey) {
-        filter['#p'] = [serverPubkey];
-      }
-    }
-    
-    return filter;
+    return this.eventService.createKind21120Filter(showAllEvents);
   }
 
-  /**
-   * Get the logged-in user's public key from localStorage
-   * @returns The public key or null if not found
-   */
-  public getLoggedInPubkey(): string | null {
-    const pubkey = localStorage.getItem('userPublicKey');
-    
-    // If no pubkey in localStorage, try to get it from window.nostr directly
-    if (!pubkey && window.nostr && typeof window.nostr.getPublicKey === 'function') {
-      // Note: This returns a promise, so we can't use it synchronously
-      // But we'll trigger the fetch so it might be available next time
-      window.nostr.getPublicKey()
-        .then(directPubkey => {
-          localStorage.setItem('userPublicKey', directPubkey);
-          return directPubkey;
-        })
-        .catch(() => {
-          return null;
-        });
-    }
-    
-    return pubkey;
-  }
+  // Profile methods delegated to NostrEventService
 
   /**
    * Fetch profile data for a pubkey
@@ -711,161 +243,24 @@ export class NostrService {
    * @returns A promise that resolves to ProfileData or null
    */
   public async fetchProfileData(pubkey: string): Promise<ProfileData | null> {
-    // Return from cache if available
-    if (this.profileCache.has(pubkey)) {
-      return this.profileCache.get(pubkey) || null;
-    }
-    
-    try {
-      // List of relays to try, in order of preference
-      const relays = ['wss://relay.damus.io', 'wss://relay.nostr.band', 'wss://nos.lol'];
-      
-      // Prepare the filter for kind-0 events (profile metadata)
-      const filter = {
-        kinds: [0],
-        authors: [pubkey],
-        limit: 1
-      };
-      
-      // Create unique request ID
-      const requestId = `profile-${Math.random().toString(36).substring(2, 10)}`;
-      const subRequest = ["REQ", requestId, filter];
-      
-      // Store events received
-      const events: NostrEvent[] = [];
-      let connected = false;
-      
-      // Try each relay until we get a response
-      for (const relayUrl of relays) {
-        if (connected) {
-          break;
-        }
-        
-        try {
-          const wsManager = new WebSocketManager();
-          await wsManager.connect(relayUrl, {
-            timeout: 3000,
-            onOpen: (ws) => {
-              // Send subscription request for profile
-              ws.send(JSON.stringify(subRequest));
-            },
-            onMessage: (data) => {
-              // Type assertion for the received data
-              const nostrData = data as unknown[];
-              
-              // If we receive an EVENT message with our request ID
-              if (Array.isArray(nostrData) && nostrData[0] === 'EVENT' && nostrData[1] === requestId) {
-                events.push(nostrData[2] as NostrEvent);
-                connected = true;
-                wsManager.close();
-              }
-              // If we receive EOSE (End of Stored Events)
-              else if (Array.isArray(nostrData) && nostrData[0] === 'EOSE' && nostrData[1] === requestId) {
-                wsManager.close();
-              }
-            }
-          });
-        } catch {
-          // If connection fails, just continue to next relay
-        }
-      }
-      
-      // Process the events if we found any
-      if (events.length > 0) {
-        const profileEvent = events[0];
-        try {
-          // Parse the content as JSON to get profile data
-          const profileData = JSON.parse(profileEvent.content) as ProfileData;
-          
-          // Store in cache
-          this.profileCache.set(pubkey, profileData);
-          
-          return profileData;
-        } catch {
-          // If parsing fails, return null
-          return null;
-        }
-      }
-    } catch {
-      // Any error, return null
-    }
-    
-    return null;
+    return this.eventService.fetchProfileData(pubkey);
   }
 
   /**
-   * Update the status via callback if set
-   * @param message The status message
-   * @param className The CSS class name for styling
+   * Get the logged-in user's public key from localStorage
+   * @returns The public key or null if not found
    */
-  private updateStatus(message: string, className: string): void {
-    if (this.statusCallback) {
-      const callback = this.statusCallback;
-      callback(message, className);
-    }
+  public getLoggedInPubkey(): string | null {
+    return localStorage.getItem('userPublicKey');
   }
 
-  /**
-   * Cache 31120 events by relay URL
-   * @param relayUrl The relay URL used as cache key
-   * @param events Array of 31120 events to cache
-   */
-  private cacheEvents(relayUrl: string, events: NostrEvent[]): void {
-    // Create a map of events by server pubkey
-    const eventsMap: {[key: string]: NostrEvent} = {};
-    
-    for (const event of events) {
-      // Find the d tag which contains the server pubkey
-      const dTag = event.tags.find((tag: string[]) => tag[0] === 'd');
-      if (dTag && dTag.length > 1) {
-        const serverPubkey = dTag[1];
-        // Store the event with the server pubkey as the key
-        eventsMap[serverPubkey] = event;
-      }
-    }
-    
-    const timestamp = Date.now();
-    const cacheData: [number, {[key: string]: NostrEvent}] = [timestamp, eventsMap];
-    
-    // Store in memory cache
-    this.events31120Cache.set(relayUrl, cacheData);
-    
-    // Store in localStorage for persistence between page navigations
-    try {
-      localStorage.setItem(this.localStorageCachePrefix + relayUrl, JSON.stringify(cacheData));
-      console.log(`Cached ${Object.keys(eventsMap).length} 31120 events for ${relayUrl} in memory and localStorage`);
-    } catch (error) {
-      console.error('Failed to store cache in localStorage:', error);
-      // Still keep the in-memory cache even if localStorage fails
-    }
-  }
+  // Cache methods delegated to NostrCacheService
 
   /**
-   * Clear the 31120 events cache for a specific relay or all relays
+   * Clear the events cache for a specific relay or all relays
    * @param relayUrl Optional relay URL to clear cache for. If not provided, clears all caches.
    */
   public clearEventsCache(relayUrl?: string): void {
-    if (relayUrl) {
-      // Clear in-memory cache
-      this.events31120Cache.delete(relayUrl);
-      
-      // Clear localStorage cache
-      localStorage.removeItem(this.localStorageCachePrefix + relayUrl);
-      
-      console.log(`Cleared cache for ${relayUrl} (memory and localStorage)`);
-    } else {
-      // Clear all in-memory caches
-      this.events31120Cache.clear();
-      
-      // Clear all localStorage caches
-      for (let i = 0; i < localStorage.length; i++) {
-        const key = localStorage.key(i);
-        if (key && key.startsWith(this.localStorageCachePrefix)) {
-          localStorage.removeItem(key);
-        }
-      }
-      
-      console.log('Cleared all event caches (memory and localStorage)');
-    }
+    this.cacheService.clearEventsCache(relayUrl);
   }
 }
\ No newline at end of file
diff --git a/client/src/services/NostrUtils.ts b/client/src/services/NostrUtils.ts
new file mode 100644
index 0000000..e3688f0
--- /dev/null
+++ b/client/src/services/NostrUtils.ts
@@ -0,0 +1,220 @@
+/**
+ * NostrUtils.ts
+ * Utility functions for Nostr protocol operations
+ */
+
+// External imports
+import * as nostrTools from 'nostr-tools';
+
+// Project imports
+import type { NostrEvent } from '../relay';
+import { convertNpubToHex } from '../relay';
+
+/**
+ * Interface for profile data
+ */
+export interface ProfileData {
+  name?: string;
+  about?: string;
+  picture?: string;
+  nip05?: string;
+  [key: string]: unknown;
+}
+
+/**
+ * Fetch a user's profile metadata from a relay
+ */
+export async function fetchUserProfile(pubkey: string, relays: string[] = ['wss://relay.damus.io', 'wss://relay.nostr.band', 'wss://nos.lol']): Promise<ProfileData | null> {
+  try {
+    // Prepare the filter for kind-0 events (profile metadata)
+    const filter = {
+      kinds: [0],
+      authors: [pubkey],
+      limit: 1
+    };
+    
+    // Create unique request ID
+    const requestId = `profile-${Math.random().toString(36).substring(2, 10)}`;
+    const subRequest = ["REQ", requestId, filter];
+    
+    // Store events received
+    const events: NostrEvent[] = [];
+    let connected = false;
+    
+    // Try each relay until we get a response
+    for (const relayUrl of relays) {
+      if (connected) {
+        break;
+      }
+      
+      try {
+        await new Promise<void>((resolve, reject) => {
+          const ws = new WebSocket(relayUrl);
+          const timeout = setTimeout(() => {
+            try { ws.close(); } catch {}
+            reject(new Error('Timeout'));
+          }, 3000);
+          
+          ws.onopen = () => {
+            // Send subscription request for profile
+            ws.send(JSON.stringify(subRequest));
+          };
+          
+          ws.onmessage = (msg) => {
+            try {
+              const data = JSON.parse(msg.data);
+              
+              // If we receive an EVENT message with our request ID
+              if (Array.isArray(data) && data[0] === 'EVENT' && data[1] === requestId) {
+                events.push(data[2] as NostrEvent);
+                connected = true;
+                clearTimeout(timeout);
+                ws.close();
+                resolve();
+              }
+              // If we receive EOSE (End of Stored Events)
+              else if (Array.isArray(data) && data[0] === 'EOSE' && data[1] === requestId) {
+                clearTimeout(timeout);
+                ws.close();
+                resolve();
+              }
+            } catch {
+              // Ignore parsing errors
+            }
+          };
+          
+          ws.onerror = () => {
+            clearTimeout(timeout);
+            reject();
+          };
+          
+          ws.onclose = () => {
+            clearTimeout(timeout);
+            resolve();
+          };
+        });
+      } catch {
+        // Continue to next relay on error
+      }
+    }
+    
+    // Process the events if we found any
+    if (events.length > 0) {
+      try {
+        // Parse the content as JSON to get profile data
+        return JSON.parse(events[0].content) as ProfileData;
+      } catch {
+        return null;
+      }
+    }
+  } catch {
+    // Return null on any error
+  }
+  
+  return null;
+}
+
+/**
+ * Get the server pubkey from an event
+ */
+export function getServerPubkeyFromEvent(event: NostrEvent): string | null {
+  const dTag = event.tags.find(tag => tag[0] === 'd');
+  if (dTag && dTag.length > 1) {
+    return dTag[1];
+  }
+  return null;
+}
+
+/**
+ * Check if a server event is expired
+ */
+export function isServerEventExpired(event: NostrEvent): boolean {
+  const expiryTag = event.tags.find(tag => tag[0] === 'expiry');
+  if (expiryTag && expiryTag.length > 1) {
+    try {
+      const expiryTime = parseInt(expiryTag[1]);
+      const now = Math.floor(Date.now() / 1000);
+      return expiryTime < now;
+    } catch {
+      return false;
+    }
+  }
+  return false;
+}
+
+/**
+ * Convert a potentially npub pubkey to hex format
+ */
+export function ensureHexPubkey(pubkeyInput: string): string {
+  // Already a valid hex pubkey
+  if (/^[0-9a-f]{64}$/i.test(pubkeyInput)) {
+    return pubkeyInput;
+  }
+  
+  // Try to convert from npub
+  if (pubkeyInput.startsWith('npub')) {
+    const hexPubkey = convertNpubToHex(pubkeyInput);
+    if (hexPubkey) {
+      return hexPubkey;
+    }
+  }
+  
+  // Return original if conversion fails
+  return pubkeyInput;
+}
+
+/**
+ * Attempt to get a user's public key from NIP-07 extension
+ */
+export async function getUserPubkey(): Promise<string | null> {
+  try {
+    // Try to get pubkey from window.nostr
+    if (window.nostr && typeof window.nostr.getPublicKey === 'function') {
+      const pubkey = await window.nostr.getPublicKey();
+      if (pubkey) {
+        localStorage.setItem('userPublicKey', pubkey);
+        return pubkey;
+      }
+    }
+    
+    // Fall back to localStorage
+    return localStorage.getItem('userPublicKey');
+  } catch {
+    return localStorage.getItem('userPublicKey');
+  }
+}
+
+/**
+ * Create a relay connection filter for different types of events
+ */
+export function createEventFilter(kind: number, options: {
+  authors?: string[],
+  since?: number,
+  until?: number,
+  limit?: number,
+  pubkeyTag?: string
+} = {}): Record<string, unknown> {
+  const filter: Record<string, unknown> = { kinds: [kind] };
+  
+  if (options.authors && options.authors.length > 0) {
+    filter.authors = options.authors;
+  }
+  
+  if (options.since) {
+    filter.since = options.since;
+  }
+  
+  if (options.until) {
+    filter.until = options.until;
+  }
+  
+  if (options.limit) {
+    filter.limit = options.limit;
+  }
+  
+  if (options.pubkeyTag) {
+    filter['#p'] = [options.pubkeyTag];
+  }
+  
+  return filter;
+}
\ No newline at end of file
diff --git a/client/src/services/RelayStatusManager.ts b/client/src/services/RelayStatusManager.ts
new file mode 100644
index 0000000..43532f9
--- /dev/null
+++ b/client/src/services/RelayStatusManager.ts
@@ -0,0 +1,30 @@
+/**
+ * RelayStatusManager.ts
+ * Component for managing relay connection status UI updates
+ */
+
+/**
+ * Class for managing relay status in the UI
+ */
+export class RelayStatusManager {
+  private relayStatus: HTMLElement | null = null;
+  
+  /**
+   * Initialize the relay status element
+   */
+  public initialize(): void {
+    this.relayStatus = document.getElementById('relayStatus');
+  }
+  
+  /**
+   * Update the relay status display
+   * @param message Status message
+   * @param className CSS class for styling (connected, error, etc.)
+   */
+  public updateRelayStatus(message: string, className: string): void {
+    if (this.relayStatus) {
+      this.relayStatus.textContent = message;
+      this.relayStatus.className = `relay-status ${className}`;
+    }
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/ToastNotifier.ts b/client/src/services/ToastNotifier.ts
new file mode 100644
index 0000000..9240f99
--- /dev/null
+++ b/client/src/services/ToastNotifier.ts
@@ -0,0 +1,159 @@
+/**
+ * ToastNotifier.ts
+ * Service for showing toast notifications to the user
+ */
+
+/**
+ * Type for notification types with associated styling
+ */
+type NotificationType = 'success' | 'error' | 'warning' | 'info';
+
+/**
+ * Class for showing toast notifications
+ */
+export class ToastNotifier {
+  private static toastContainer: HTMLElement | null = null;
+  private static initialized = false;
+  
+  /**
+   * Initialize the toast container
+   */
+  private static initialize(): void {
+    if (this.initialized) return;
+    
+    this.toastContainer = document.getElementById('toast-container');
+    
+    // Create container if it doesn't exist
+    if (!this.toastContainer) {
+      this.toastContainer = document.createElement('div');
+      this.toastContainer.id = 'toast-container';
+      this.toastContainer.className = 'toast-container';
+      document.body.appendChild(this.toastContainer);
+      
+      // Add styles if not already present
+      if (!document.getElementById('toast-styles')) {
+        const style = document.createElement('style');
+        style.id = 'toast-styles';
+        style.textContent = `
+          .toast-container {
+            position: fixed;
+            top: 20px;
+            right: 20px;
+            z-index: 1000;
+            display: flex;
+            flex-direction: column;
+            gap: 10px;
+            max-width: 300px;
+          }
+          
+          .toast {
+            padding: 12px 16px;
+            border-radius: 4px;
+            color: white;
+            font-size: 14px;
+            opacity: 0;
+            transform: translateX(50px);
+            transition: opacity 0.3s, transform 0.3s;
+            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
+            position: relative;
+            margin-bottom: 8px;
+          }
+          
+          .toast.visible {
+            opacity: 1;
+            transform: translateX(0);
+          }
+          
+          .toast.success {
+            background-color: #4CAF50;
+          }
+          
+          .toast.error {
+            background-color: #F44336;
+          }
+          
+          .toast.warning {
+            background-color: #FF9800;
+          }
+          
+          .toast.info {
+            background-color: #2196F3;
+          }
+          
+          .toast-close {
+            position: absolute;
+            top: 8px;
+            right: 8px;
+            cursor: pointer;
+            font-size: 16px;
+            opacity: 0.7;
+          }
+          
+          .toast-close:hover {
+            opacity: 1;
+          }
+        `;
+        document.head.appendChild(style);
+      }
+    }
+    
+    this.initialized = true;
+  }
+  
+  /**
+   * Show a toast notification
+   * @param message The message to show
+   * @param type The type of notification
+   * @param duration The duration in milliseconds
+   */
+  public static show(message: string, type: NotificationType = 'info', duration = 5000): void {
+    this.initialize();
+    
+    if (!this.toastContainer) return;
+    
+    // Create toast element
+    const toast = document.createElement('div');
+    toast.className = `toast ${type}`;
+    toast.innerHTML = `
+      ${message}
+      <span class="toast-close">&times;</span>
+    `;
+    
+    // Add close button handler
+    const closeBtn = toast.querySelector('.toast-close');
+    if (closeBtn) {
+      closeBtn.addEventListener('click', () => {
+        this.removeToast(toast);
+      });
+    }
+    
+    // Add to container
+    this.toastContainer.appendChild(toast);
+    
+    // Trigger reflow to ensure transition works
+    void toast.offsetWidth;
+    
+    // Show the toast
+    toast.classList.add('visible');
+    
+    // Set timeout to remove
+    setTimeout(() => {
+      this.removeToast(toast);
+    }, duration);
+  }
+  
+  /**
+   * Remove a toast from the container
+   * @param toast The toast element to remove
+   */
+  private static removeToast(toast: HTMLElement): void {
+    toast.classList.remove('visible');
+    
+    // Wait for transition to complete
+    setTimeout(() => {
+      if (toast.parentNode === this.toastContainer) {
+        this.toastContainer?.removeChild(toast);
+      }
+    }, 300);
+  }
+}
\ No newline at end of file
diff --git a/client/src/services/UiService.ts b/client/src/services/UiService.ts
index 037e9ca..0bde571 100644
--- a/client/src/services/UiService.ts
+++ b/client/src/services/UiService.ts
@@ -2,17 +2,18 @@
  * UiService.ts
  * Handles UI-related operations and DOM manipulation
  */
-
-import jsQR from 'jsqr';
 import * as nostrTools from 'nostr-tools';
-
-import type { NostrEvent } from '../relay';
-
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
+import { NostrEvent } from '../relay';
 import { HttpService } from './HttpService';
-// eslint-disable-next-line @typescript-eslint/consistent-type-imports
+import { HttpClient } from './HttpClient';
 import { NostrService } from './NostrService';
-import type { ProfileData, ReceivedEvent } from './NostrService';
+import { ReceivedEvent } from './NostrEventService';
+import { RelayStatusManager } from './RelayStatusManager';
+import { EventListRenderer } from './EventListRenderer';
+import { EventDetailsRenderer } from './EventDetailsRenderer';
+import { Nostr21121ResponseHandler } from './Nostr21121ResponseHandler';
+import { ToastNotifier } from './ToastNotifier';
+import { HttpFormatter } from './HttpFormatter';
 
 /**
  * Class for managing UI operations
@@ -20,12 +21,17 @@ import type { ProfileData, ReceivedEvent } from './NostrService';
 export class UiService {
   private nostrService: NostrService;
   private httpService: HttpService;
-  private receivedEvents = new Map<string, ReceivedEvent>();
+  private httpClient: HttpClient;
   
-  // DOM Elements
-  private relayStatus: HTMLElement | null = null;
-  private eventsList: HTMLElement | null = null;
-  private eventDetails: HTMLElement | null = null;
+  // Component managers
+  private relayStatusManager: RelayStatusManager;
+  private eventListRenderer: EventListRenderer;
+  private eventDetailsRenderer: EventDetailsRenderer;
+  private responseHandler: Nostr21121ResponseHandler;
+  
+  // Data stores
+  private receivedEvents = new Map<string, ReceivedEvent>();
+  private relatedEvents: Map<string, string[]> = new Map<string, string[]>(); // Map of event IDs to related event IDs
   
   /**
    * Constructor
@@ -35,996 +41,344 @@ export class UiService {
   constructor(nostrService: NostrService, httpService: HttpService) {
     this.nostrService = nostrService;
     this.httpService = httpService;
+    this.httpClient = new HttpClient(httpService);
+    
+    // Initialize component managers
+    this.relayStatusManager = new RelayStatusManager();
+    this.eventListRenderer = new EventListRenderer();
+    this.eventDetailsRenderer = new EventDetailsRenderer(this.receivedEvents, this.relatedEvents);
+    this.responseHandler = new Nostr21121ResponseHandler(this.nostrService, this.relatedEvents);
   }
-
+  
   /**
    * Initialize the UI elements and event listeners
    */
   public initialize(): void {
-    // Get DOM elements
-    this.relayStatus = document.getElementById('relayStatus');
-    this.eventsList = document.getElementById('eventsList');
-    this.eventDetails = document.getElementById('eventDetails');
+    // Initialize component managers
+    this.relayStatusManager.initialize();
+    this.eventListRenderer.initialize();
+    this.eventDetailsRenderer.initialize();
     
-    // Initialize event handlers
-    this.setupTabNavigation();
-    this.setupQRScanner();
-    this.setupRawTextInput();
-    this.setupEventHandlers();
-    
-    // Set event handler for Nostr events
-    this.nostrService.setEventHandler((event) => this.processEvent(event));
-    
-    // Apply initial filtering based on checkbox state
-    setTimeout(() => {
-      const showAllEventsCheckbox = document.getElementById('showAllEvents') as HTMLInputElement;
-      if (showAllEventsCheckbox) {
-        this.filterEventsInUI(showAllEventsCheckbox.checked);
-      }
-    }, 500); // Short delay to ensure UI is fully initialized
+    // Set up event listeners for the UI
+    this.setupEventListeners();
   }
-
+  
   /**
-   * Set up tab navigation for the UI
+   * Set up event listeners for the UI
    */
-  private setupTabNavigation(): void {
-    const tabs = document.querySelectorAll('.tab-content');
-    const tabButtons = document.querySelectorAll('.tab-btn');
-    
-    tabButtons.forEach(button => {
-      button.addEventListener('click', () => {
-        // Remove active class from all buttons and tabs
-        tabButtons.forEach(b => b.classList.remove('active'));
-        tabs.forEach(tab => tab.classList.remove('active'));
+  private setupEventListeners(): void {
+    // Get event list element
+    const eventsList = this.eventListRenderer.getEventsList();
+    if (eventsList) {
+      // Add click listener for events in the list
+      eventsList.addEventListener('click', (event) => {
+        const target = event.target as HTMLElement;
+        const eventItem = target.closest('.event-item') as HTMLElement;
         
-        // Add active class to clicked button and corresponding tab
-        button.classList.add('active');
-        const tabId = button.getAttribute('data-tab');
-        
-        if (tabId) {
-          const activeTab = document.getElementById(tabId);
-          if (activeTab) {
-            activeTab.classList.add('active');
+        if (eventItem) {
+          const eventId = eventItem.dataset.id;
+          if (eventId) {
+            // Show event details
+            this.showEventDetails(eventId);
           }
         }
       });
-    });
-  }
-
-  /**
-   * Set up QR scanner functionality
-   */
-  private setupQRScanner(): void {
-    const scannerContainer = document.getElementById('qrScannerContainer');
-    const startScanBtn = document.getElementById('startScanBtn');
-    const stopScanBtn = document.getElementById('stopScanBtn');
-    const videoElement = document.getElementById('qrVideo') as HTMLVideoElement;
-    const qrResultInput = document.getElementById('qrResultInput') as HTMLTextAreaElement;
-    
-    if (!scannerContainer || !startScanBtn || !stopScanBtn || !videoElement || !qrResultInput) {
-      return;
     }
     
-    // Start QR scanner
-    async function startQRScanner(): Promise<void> {
-      try {
-        if (scannerContainer && startScanBtn && stopScanBtn) {
-          scannerContainer.classList.remove('hidden');
-          startScanBtn.classList.add('hidden');
-          stopScanBtn.classList.remove('hidden');
-        }
-        
-        // Get user media
-        const stream = await navigator.mediaDevices.getUserMedia({
-          video: { facingMode: 'environment' }
-        });
-        
-        videoElement.srcObject = stream;
-        videoElement.play();
-        
-        // Start scanning for QR codes
-        scanQRCode();
-      } catch (error) {
-        alert(`Error accessing camera: ${error instanceof Error ? error.message : String(error)}`);
-        stopQRScanner();
-      }
-    }
-    
-    // Stop QR scanner
-    function stopQRScanner(): void {
-      if (scannerContainer && startScanBtn && stopScanBtn) {
-        scannerContainer.classList.add('hidden');
-        startScanBtn.classList.remove('hidden');
-        stopScanBtn.classList.add('hidden');
-      }
-      
-      // Stop the video stream
-      if (videoElement.srcObject) {
-        const stream = videoElement.srcObject as MediaStream;
-        const tracks = stream.getTracks();
-        
-        tracks.forEach(track => {
-          track.stop();
-        });
-        
-        videoElement.srcObject = null;
-      }
-    }
-    
-    // Add event listeners
-    startScanBtn.addEventListener('click', () => {
-      startQRScanner().catch(error => {
-        alert(`Error starting QR scanner: ${error instanceof Error ? error.message : String(error)}`);
-      });
-    });
-    
-    stopScanBtn.addEventListener('click', stopQRScanner);
-    
-    // Function to scan QR code from video feed
-    function scanQRCode(): void {
-      if (videoElement.readyState !== videoElement.HAVE_ENOUGH_DATA) {
-        // Not enough data yet, retry after a short delay
-        setTimeout(scanQRCode, 100);
-        return;
-      }
-      
-      const canvas = document.createElement('canvas');
-      const width = videoElement.videoWidth;
-      const height = videoElement.videoHeight;
-      
-      canvas.width = width;
-      canvas.height = height;
-      
-      const ctx = canvas.getContext('2d');
-      if (!ctx) {
-        setTimeout(scanQRCode, 100);
-        return;
-      }
-      
-      ctx.drawImage(videoElement, 0, 0, width, height);
-      const imageData = ctx.getImageData(0, 0, width, height);
-      
-      // Use jsQR to detect QR code
-      const code = jsQR(imageData.data, width, height, {
-        inversionAttempts: 'dontInvert'
-      });
-      
-      if (code) {
-        // QR code detected
-        qrResultInput.value = code.data;
-        stopQRScanner();
-      } else {
-        // No QR code found, retry after a short delay
-        setTimeout(scanQRCode, 100);
-      }
-    }
-  }
-
-  /**
-   * Set up raw text input functionality
-   */
-  private setupRawTextInput(): void {
-    const rawEventInput = document.getElementById('rawEventInput') as HTMLTextAreaElement;
-    const parseRawEventBtn = document.getElementById('parseRawEventBtn');
-    
-    if (!rawEventInput || !parseRawEventBtn) {
-      return;
-    }
-    
-    parseRawEventBtn.addEventListener('click', () => {
-      try {
-        const rawText = rawEventInput.value.trim();
-        if (!rawText) {
-          alert('Please enter a raw Nostr event.');
-          return;
-        }
-        
-        // Try to parse as JSON
-        const eventData = JSON.parse(rawText);
-        
-        // Validate as a Nostr event
-        if (!eventData.id || !eventData.pubkey || !eventData.created_at || 
-            !eventData.kind || !Array.isArray(eventData.tags) || 
-            typeof eventData.content !== 'string' || !eventData.sig) {
-          alert('Invalid Nostr event format.');
-          return;
-        }
-        
-        // Process the event
-        this.processEvent(eventData as NostrEvent);
-        
-        // Clear the input
-        rawEventInput.value = '';
-        
-        // Show notification
-        alert('Event processed successfully!');
-      } catch (error) {
-        alert(`Error parsing event: ${error instanceof Error ? error.message : String(error)}`);
-      }
-    });
-  }
-
-  /**
-   * Set up event handlers for the UI
-   */
-  private setupEventHandlers(): void {
-    // Connect to relay button
-    const connectRelayBtn = document.getElementById('connectRelayBtn');
-    const relayUrlInput = document.getElementById('relayUrlInput') as HTMLInputElement;
-    
-    if (connectRelayBtn && relayUrlInput) {
-      connectRelayBtn.addEventListener('click', async () => {
-        const relayUrl = relayUrlInput.value.trim();
-        if (!relayUrl) {
-          alert('Please enter a relay URL');
-          return;
-        }
-        
-        try {
-          // Connect to the relay
-          const success = await this.nostrService.connectToRelay(relayUrl);
-          
-          if (success) {
-            // Subscribe to events
-            const showAllEventsCheckbox = document.getElementById('showAllEvents') as HTMLInputElement;
-            const showAllEvents = showAllEventsCheckbox ? showAllEventsCheckbox.checked : false;
-            
-            const filter = this.nostrService.createKind21120Filter(showAllEvents);
-            await this.nostrService.subscribeToEvents(filter);
-          }
-        } catch (error) {
-          alert(`Error connecting to relay: ${error instanceof Error ? error.message : String(error)}`);
-        }
-      });
-    }
-    
-    // Show all events checkbox
+    // Setup filter for server-addressed events
     const showAllEventsCheckbox = document.getElementById('showAllEvents') as HTMLInputElement;
     if (showAllEventsCheckbox) {
-      showAllEventsCheckbox.addEventListener('change', async () => {
-        try {
-          // Resubscribe with new filter
-          const filter = this.nostrService.createKind21120Filter(showAllEventsCheckbox.checked);
-          await this.nostrService.subscribeToEvents(filter);
-          
-          // Also filter existing events in the UI
-          this.filterEventsInUI(showAllEventsCheckbox.checked);
-        } catch (error) {
-          alert(`Error updating subscription: ${error instanceof Error ? error.message : String(error)}`);
-        }
+      showAllEventsCheckbox.addEventListener('change', () => {
+        this.eventListRenderer.filterEventsInUI(showAllEventsCheckbox.checked);
       });
     }
-    
-    // Connect to default relay on page load
-    setTimeout(async () => {
-      if (relayUrlInput && relayUrlInput.value && connectRelayBtn) {
-        // Trigger click on the connect button
-        connectRelayBtn.click();
-      }
-    }, 500);
   }
-
+  
   /**
-   * Process a received Nostr event
-   * @param event The Nostr event to process
+   * Update the relay status
+   * @param message Status message
+   * @param className CSS class for styling
    */
-  private processEvent(event: NostrEvent): void {
-    // Ensure event has an ID
-    if (!event.id) {
-      return;
-    }
-    
-    // Check if this is a new event
-    if (this.receivedEvents.has(event.id)) {
-      return;
-    }
+  public updateRelayStatus(message: string, className: string): void {
+    this.relayStatusManager.updateRelayStatus(message, className);
+  }
+  
+  /**
+   * Add a received event to the UI
+   * @param receivedEvent The received event data
+   */
+  public addReceivedEvent(receivedEvent: ReceivedEvent): void {
+    const event = receivedEvent.event;
+    if (!event || !event.id) return;
     
     // Store the event
-    const receivedEvent: ReceivedEvent = {
-      id: event.id,
-      event: event,
-      receivedAt: Date.now(),
-      decrypted: false
-    };
-    
     this.receivedEvents.set(event.id, receivedEvent);
     
-    // Add event to UI
-    this.addEventToUI(receivedEvent);
-  }
-
-  /**
-   * Add an event to the UI
-   * @param receivedEvent The received event to add to the UI
-   */
-  private addEventToUI(receivedEvent: ReceivedEvent): void {
-    if (!this.eventsList) {
-      return;
+    // Check for related events
+    this.checkForRelatedEvents(event);
+    
+    // Add to UI
+    this.eventListRenderer.addEventToList(event, event.kind);
+    
+    // Check if this is a response to a known request
+    if (event.kind === 21121) {
+      this.handleResponseEvent(event);
     }
+  }
+  
+  /**
+   * Check for related events (requests/responses)
+   * @param event The event to check relations for
+   */
+  private checkForRelatedEvents(event: NostrEvent): void {
+    if (!event.id) return;
     
-    const event = receivedEvent.event;
-    
-    // Create event item with improved styling
-    const eventItem = document.createElement('div');
-    eventItem.className = 'event-item';
-    eventItem.dataset.id = event.id;
-    
-    // Get event ID for display
-    const eventIdForDisplay = event.id ? event.id.substring(0, 8) : 'unknown';
-    
-    // Determine if it's a request or response
-    const hasP = event.tags.some(tag => tag[0] === 'p');
-    const hasE = event.tags.some(tag => tag[0] === 'e');
-    const eventType = hasP ? 'HTTP Request' : (hasE ? 'HTTP Response' : 'Unknown');
-    
-    // Find recipient if available and check if it's addressed to our server
-    let recipient = '';
-    let isToServer = false;
-    const pTag = event.tags.find(tag => tag[0] === 'p');
-    
-    if (pTag && pTag.length > 1) {
-      recipient = `| To: ${pTag[1].substring(0, 8)}...`;
-      
-      // Check if this message is addressed to our server
-      try {
-        // Get the server pubkey from localStorage
-        const serverNsec = localStorage.getItem('serverNsec');
-        if (serverNsec) {
-          const decoded = nostrTools.nip19.decode(serverNsec);
-          if (decoded.type === 'nsec') {
-            // Convert Uint8Array to hex string
-            const privateKeyHex = Array.from(decoded.data as Uint8Array)
-              .map(b => b.toString(16).padStart(2, '0'))
-              .join('');
-            const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
-            const serverPubkey = nostrTools.getPublicKey(privateKeyBytes);
-            
-            // Check if the p tag matches our server pubkey
-            isToServer = (pTag[1] === serverPubkey);
+    // Check if this is a response with an e tag referencing a request
+    if (event.kind === 21121) {
+      const eTags = event.tags.filter(tag => tag[0] === 'e');
+      eTags.forEach(eTag => {
+        if (eTag.length > 1) {
+          const requestId = eTag[1];
+          // Create relationship: request → response
+          const responses = this.relatedEvents.get(requestId) || [];
+          if (!responses.includes(event.id!)) {
+            responses.push(event.id!);
+            this.relatedEvents.set(requestId, responses);
           }
         }
-      } catch {
-        // Ignore errors when checking
-      }
+      });
     }
-    
-    // Format the event item
-    eventItem.innerHTML = `
-      <div class="event-item-container">
-        <div class="event-avatar" data-pubkey="${event.pubkey}">
-          <div class="avatar-placeholder">👤</div>
-        </div>
-        <div class="event-content-wrapper">
-          <div class="event-header">
-            <div class="event-type ${eventType === 'HTTP Request' ? 'request' : 'response'}">${eventType}</div>
-            <div class="event-time">${new Date(event.created_at * 1000).toLocaleTimeString()}</div>
-          </div>
-          <div class="event-id">ID: ${eventIdForDisplay}... ${recipient} ${isToServer ? '<span class="server-match">✓</span>' : ''}</div>
-          <div class="event-pubkey">From: ${event.pubkey.substring(0, 8)}...</div>
-        </div>
-      </div>
-    `;
-    
-    // Set a data attribute to indicate if this event is addressed to our server
-    eventItem.dataset.toServer = isToServer.toString();
-    
-    // Add to list at the top
-    if (this.eventsList.firstChild) {
-      this.eventsList.insertBefore(eventItem, this.eventsList.firstChild);
-    } else {
-      this.eventsList.appendChild(eventItem);
-    }
-    
-    // Fetch profile for avatar after a short delay
-    const avatarElement = eventItem.querySelector('.event-avatar') as HTMLElement;
-    if (avatarElement) {
-      setTimeout(() => {
-        this.nostrService.fetchProfileData(event.pubkey)
-          .then(profile => this.updateAvatarElement(avatarElement, profile))
-          .catch(() => this.updateAvatarElement(avatarElement, null));
-      }, 10);
-    }
-    
-    // Add click event to show details
-    eventItem.addEventListener('click', () => {
-      if (event.id) {
-        this.showEventDetails(event.id);
+  }
+  
+  /**
+   * Handle a response event (NIP-21121)
+   * @param event The response event
+   */
+  private handleResponseEvent(event: NostrEvent): void {
+    // Find the request event this response is for
+    const eTags = event.tags.filter(tag => tag[0] === 'e');
+    eTags.forEach(eTag => {
+      if (eTag.length > 1) {
+        const requestId = eTag[1];
+        // Add a visual indicator to the request event in the UI
+        const eventItem = document.querySelector(`.event-item[data-id="${requestId}"]`);
+        if (eventItem && !eventItem.querySelector('.response-indicator')) {
+          const responseIndicator = document.createElement('div');
+          responseIndicator.className = 'response-indicator';
+          responseIndicator.innerHTML = '<span class="response-available">21121 Response Available</span>';
+          eventItem.appendChild(responseIndicator);
+        }
       }
     });
   }
-
+  
   /**
-   * Update an avatar element with profile data
-   * @param element The element to update
-   * @param profile The profile data to use
-   */
-  private updateAvatarElement(element: HTMLElement, profile: ProfileData | null): void {
-    if (!element) {
-      return;
-    }
-    
-    if (profile && profile.picture) {
-      // Use the profile picture URL
-      element.innerHTML = `<img src="${profile.picture}" alt="Avatar" class="avatar-img" />`;
-    } else {
-      // Use default avatar
-      element.innerHTML = `<div class="avatar-placeholder">👤</div>`;
-    }
-    
-    // Make sure it's visible
-    element.style.opacity = '1';
-  }
-
-  /**
-   * Show event details in the UI
-   * @param eventId The ID of the event to show details for
+   * Show event details
+   * @param eventId The event ID to show details for
    */
   public showEventDetails(eventId: string): void {
-    if (!this.eventDetails) {
-      console.error('Event details element not found!');
-      return;
-    }
+    this.eventDetailsRenderer.showEventDetails(eventId);
     
-    const receivedEvent = this.receivedEvents.get(eventId);
-    if (!receivedEvent) {
-      console.error(`Event with ID ${eventId} not found in receivedEvents Map!`);
-      return;
-    }
+    // Set up event handlers for interactive elements
+    const eventDetailsElement = this.eventDetailsRenderer.getEventDetailsElement();
+    if (!eventDetailsElement) return;
     
-    const event = receivedEvent.event;
+    // Handle "Execute HTTP Request" button
+    const executeButtons = eventDetailsElement.querySelectorAll('.execute-http-request-btn');
+    executeButtons.forEach(button => {
+      button.addEventListener('click', () => this.executeHttpRequest(eventId));
+    });
     
-    // Ensure event has an ID (should already be verified)
-    const eventIdForDisplay = event.id ? event.id.substring(0, 8) : 'unknown';
-    
-    // Determine if this is a request or response
-    const isRequest = event.tags.some(tag => tag[0] === 'p');
-    const isResponse = event.tags.some(tag => tag[0] === 'e');
-    const eventTypeLabel = isRequest ? 'HTTP Request' : (isResponse ? 'HTTP Response' : 'Unknown Type');
-    
-    // Get the decrypted or original content
-    const httpContent = receivedEvent.decrypted ?
-      (receivedEvent.decryptedContent || event.content) :
-      event.content;
-    
-    // Create raw JSON representation of the event
-    const rawJson = JSON.stringify(event, null, 2);
-    
-    // Display the event details with a tabbed interface
-    this.eventDetails.innerHTML = `
-      <div class="event-detail-header">
-        <h3>${eventTypeLabel} (ID: ${eventIdForDisplay}...)</h3>
-        <div class="event-timestamp">${new Date(event.created_at * 1000).toLocaleString()}</div>
-      </div>
-      
-      <div class="event-detail-tabs">
-        <button class="tab-btn" data-tab="raw-json">Raw JSON</button>
-        <button class="tab-btn active" data-tab="http-content">HTTP Content</button>
-      </div>
-      
-      <div class="event-detail-content">
-        <div class="tab-content" id="raw-json">
-          <pre class="json-content">${rawJson}</pre>
-        </div>
-        <div class="tab-content active" id="http-content">
-          ${isRequest ?
-            `<div class="http-content-header">
-              <button class="execute-http-request-btn">Execute HTTP Request</button>
-            </div>` :
-            ''
-          }
-          <pre class="http-content">${httpContent}</pre>
-          ${!receivedEvent.decrypted ? '<div class="decryption-status" id="decryption-status-' + eventId + '">Attempting decryption...</div>' : ''}
-        </div>
-      </div>
-    `;
-    
-    // If the event isn't decrypted yet, trigger decryption
-    if (!receivedEvent.decrypted && event.id) {
-      const eventId = event.id; // Store in a const to make TypeScript happy
-      console.log(`Triggering decryption for event ${eventId.substring(0, 8)}...`);
-      // Use setTimeout to allow the UI to render first
-      setTimeout(() => {
-        this.decryptEvent(eventId).catch(error => {
-          console.error(`Error in decryption process:`, error);
-          const decryptionStatus = document.getElementById(`decryption-status-${eventId}`);
-          if (decryptionStatus) {
-            decryptionStatus.textContent = `Decryption error: ${error.message || 'Unknown error'}`;
-            decryptionStatus.classList.add('error');
-          }
-        });
-      }, 100);
-    }
-    
-    // Add event listeners for tab buttons
-    const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
-    tabButtons.forEach(button => {
-      button.addEventListener('click', () => {
-        // Get the target tab
-        const targetTabId = (button as HTMLElement).dataset.tab;
-        if (!targetTabId) {
-          return;
+    // Handle "Create Response" button
+    const createResponseBtn = eventDetailsElement.querySelector('.create-response-btn');
+    if (createResponseBtn) {
+      createResponseBtn.addEventListener('click', () => {
+        const receivedEvent = this.receivedEvents.get(eventId);
+        if (receivedEvent && receivedEvent.event) {
+          this.createResponseDialog(receivedEvent.event);
         }
-        
-        // Remove active class from all buttons and content sections
-        tabButtons.forEach(btn => btn.classList.remove('active'));
-        // Use non-null assertion since we already checked eventDetails isn't null at the beginning
-        this.eventDetails!.querySelectorAll('.tab-content').forEach(section => section.classList.remove('active'));
-        
-        // Add active class to clicked button and corresponding content section
-        button.classList.add('active');
-        const targetSection = document.getElementById(targetTabId);
-        if (targetSection) {
-          targetSection.classList.add('active');
+      });
+    }
+    
+    // Handle related event links
+    const relatedLinks = eventDetailsElement.querySelectorAll('.related-event-link');
+    relatedLinks.forEach(link => {
+      link.addEventListener('click', (e) => {
+        e.preventDefault();
+        const relatedId = (link as HTMLElement).dataset.id;
+        if (relatedId) {
+          this.showEventDetails(relatedId);
         }
       });
     });
-    
-    // Add event listener for the execute HTTP request button if it exists
-    const executeButton = this.eventDetails.querySelector('.execute-http-request-btn');
-    if (executeButton && isRequest) {
-      executeButton.addEventListener('click', async () => {
-        // Change button state to indicate processing
-        executeButton.textContent = 'Executing...';
-        executeButton.setAttribute('disabled', 'true');
-        
-        try {
-          // Execute the HTTP request
-          const response = await this.httpService.executeHttpRequest(httpContent);
-          
-          // Display the response
-          this.displayHttpResponse(response);
-        } catch (error) {
-          // Display error in a modal
-          this.displayHttpResponse(`Error executing request: ${error instanceof Error ? error.message : String(error)}`);
-        } finally {
-          // Restore button state
-          executeButton.textContent = 'Execute HTTP Request';
-          executeButton.removeAttribute('disabled');
-        }
-      });
-    }
   }
-
+  
   /**
-   * Display an HTTP response in a modal
-   * @param responseText The response text to display
+   * Execute an HTTP request based on event content
+   * @param eventId The event ID to execute
    */
-  private displayHttpResponse(responseText: string): void {
-    // Create a modal dialog element
-    const modal = document.createElement('div');
-    modal.className = 'http-response-modal';
-    
-    // Add the response content
-    modal.innerHTML = `
-      <div class="http-response-container">
-        <div class="http-response-header">
-          <h3>HTTP Response</h3>
-          <button class="close-modal-btn">&times;</button>
-        </div>
-        <div class="http-response-content">
-          <pre>${responseText}</pre>
-        </div>
-      </div>
-    `;
-    
-    // Add the modal to the DOM
-    document.body.appendChild(modal);
-    
-    // Add event listener to close the modal
-    const closeBtn = modal.querySelector('.close-modal-btn');
-    if (closeBtn) {
-      closeBtn.addEventListener('click', () => {
-        document.body.removeChild(modal);
-      });
-    }
-    
-    // Also close the modal when clicking outside the content
-    modal.addEventListener('click', (event) => {
-      if (event.target === modal) {
-        document.body.removeChild(modal);
-      }
-    });
-  }
-
-  /**
-   * Attempt to decrypt an event using NIP-44
-   * @param eventId The ID of the event to decrypt
-   */
-  public async decryptEvent(eventId: string): Promise<void> {
-    console.log(`==== DECRYPTION PROCESS STARTED FOR EVENT ${eventId.substring(0, 8)}... ====`);
-    
-    // Check if we have the event
+  private async executeHttpRequest(eventId: string): Promise<void> {
     const receivedEvent = this.receivedEvents.get(eventId);
-    if (!receivedEvent) {
-      console.error(`❌ Event with ID ${eventId.substring(0, 8)}... not found in receivedEvents Map!`);
-      return;
-    }
-    
-    // Check if already decrypted
-    if (receivedEvent.decrypted) {
-      console.log(`ℹ️ Event ${eventId.substring(0, 8)}... is already decrypted, skipping`);
-      return;
-    }
-    
-    // Check if this event is addressed to our server by finding p tag
-    const event = receivedEvent.event;
-    const pTag = event.tags.find(tag => tag[0] === 'p');
-    let isAddressedToServer = false;
-    
-    if (pTag && pTag.length > 1) {
-      // Get the server pubkey from localStorage
-      const serverNsec = localStorage.getItem('serverNsec');
-      if (serverNsec) {
-        try {
-          const decoded = nostrTools.nip19.decode(serverNsec);
-          if (decoded.type === 'nsec') {
-            // Convert Uint8Array to hex string
-            const privateKeyHex = Array.from(decoded.data as Uint8Array)
-              .map(b => b.toString(16).padStart(2, '0'))
-              .join('');
-            const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
-            const serverPubkey = nostrTools.getPublicKey(privateKeyBytes);
-            
-            // Check if the p tag matches our server pubkey
-            isAddressedToServer = (pTag[1] === serverPubkey);
-          }
-        } catch (error) {
-          console.error('Error checking if event is addressed to server:', error);
-        }
-      }
-    }
-    
-    // If not addressed to our server, mark as "decrypted" but with a note
-    if (!isAddressedToServer) {
-      console.log(`⚠️ Event ${eventId.substring(0, 8)}... is not addressed to this server, skipping decryption`);
-      receivedEvent.decrypted = true;
-      receivedEvent.decryptedContent = "[This message is not addressed to this server]";
-      this.receivedEvents.set(eventId, receivedEvent);
-      
-      // Update UI
-      const decryptionStatus = document.getElementById(`decryption-status-${eventId}`);
-      if (decryptionStatus) {
-        decryptionStatus.textContent = 'Not addressed to this server';
-        decryptionStatus.classList.add('info');
-      }
-      
-      // Update UI if this event is currently being viewed
-      const selectedEventId = this.eventDetails?.querySelector('h3')?.textContent?.match(/(.+)\.\.\./)?.[1];
-      if (selectedEventId && `${selectedEventId}...` === `${eventId.substring(0, 8)}...`) {
-        this.showEventDetails(eventId);
-      }
-      
+    if (!receivedEvent || !receivedEvent.event) {
+      ToastNotifier.show('Event not found', 'error');
       return;
     }
     
     try {
-      console.log(`✓ Found event ${eventId.substring(0, 8)}..., proceeding with decryption`);
-      const event = receivedEvent.event;
-      let decryptedContent: string;
+      // Get the HTTP content (either decrypted or raw)
+      const httpContent = receivedEvent.decrypted 
+        ? receivedEvent.decryptedContent || receivedEvent.event.content
+        : receivedEvent.event.content;
       
-      // Look for a "key" tag in the event
-      const keyTag = event.tags.find(tag => tag[0] === 'key');
+      ToastNotifier.show('Executing HTTP request...', 'info');
       
-      if (!keyTag || keyTag.length < 2) {
-        decryptedContent = `[No key tag found for decryption]\n${event.content}`;
-      } else {
-        try {
-          // Extract the encrypted key from the tag
-          const encryptedKey = keyTag[1];
-          
-          console.log(`Starting decryption process for event ${eventId.substring(0, 8)}...`);
-          
-          // Declare variable to hold the decrypted key
-          let decryptedKey: string;
-          
-          // Update the decryption status UI
-          const decryptionStatus = document.getElementById(`decryption-status-${eventId}`);
-          if (decryptionStatus) {
-            decryptionStatus.textContent = 'Getting server private key from storage...';
-          }
-          // Get the server private key from localStorage
-          console.log('🔑 Looking for server nsec in localStorage...');
-          const serverNsec = localStorage.getItem('serverNsec');
-          if (!serverNsec) {
-            console.error('❌ Server private key (nsec) not found in localStorage');
-            console.log('💡 This should be set when server is registered in NostrService.createOrUpdate31120Event');
-            
-            // Check all localStorage keys to help debug
-            console.log('📋 Available localStorage keys:');
-            for (let i = 0; i < localStorage.length; i++) {
-              const key = localStorage.key(i);
-              if (key) {
-                console.log(`   - ${key}: ${localStorage.getItem(key)?.substring(0, 20)}...`);
-              }
-            }
-            
-            if (decryptionStatus) {
-              decryptionStatus.textContent = 'Server private key (nsec) not found in localStorage';
-              decryptionStatus.classList.add('error');
-            }
-            throw new Error('Server private key (nsec) not found in localStorage');
-          }
-          
-          console.log('✓ Server nsec found in localStorage:', serverNsec.substring(0, 10) + '...');
-          
-          
-          try {
-            // Make sure nostr-tools is available
-            if (!nostrTools || !nostrTools.nip19 || !nostrTools.nip44) {
-              console.error('Required nostr-tools libraries not available');
-              if (decryptionStatus) {
-                decryptionStatus.textContent = 'Required nostr-tools libraries not available';
-                decryptionStatus.classList.add('error');
-              }
-              throw new Error('Required nostr-tools libraries not available');
-            }
-            
-            console.log('Decoding nsec to get the raw private key');
-            
-            // Let's try a completely different approach to extract the private key from nsec
-            console.log('🔑 Extracting private key from server nsec using a different method');
-            
-            let privateKeyHex: string;
-            
-            try {
-              // Example shown in the documentation uses a direct hex string
-              // The nsec format should decode to the raw private key
-              const decoded = nostrTools.nip19.decode(serverNsec);
-              
-              if (decoded.type !== 'nsec') {
-                throw new Error(`Expected nsec but got ${decoded.type}`);
-              }
-              
-              // Log the raw data for debugging
-              console.log('Raw decoded data type:', typeof decoded.data);
-              console.log('Raw decoded data length:', (decoded.data as Uint8Array).length);
-              
-              // Direct conversion to hex
-              // Convert Uint8Array to hex string
-              privateKeyHex = Array.from(decoded.data as Uint8Array)
-                .map(b => b.toString(16).padStart(2, '0'))
-                .join('');
-              console.log('✅ Successfully decoded private key from buffer:', privateKeyHex.substring(0, 8) + '...');
-              // Check if the private key looks valid (should be 64 hex chars)
-              if (privateKeyHex.length !== 64) {
-                console.warn(`⚠️ Private key has unexpected length: ${privateKeyHex.length} (expected 64)`);
-              }
-              
-              // Check if the private key looks valid (should be 64 hex chars)
-              if (privateKeyHex.length !== 64) {
-                console.warn(`⚠️ Private key has unexpected length: ${privateKeyHex.length} (expected 64)`);
-              }
-            } catch (decodeError: unknown) {
-              console.error('❌ Failed to decode nsec:', decodeError);
-              if (decryptionStatus) {
-                decryptionStatus.textContent = 'Failed to decode server key';
-                decryptionStatus.classList.add('error');
-              }
-              throw new Error(`Failed to decode server key: ${decodeError instanceof Error ? decodeError.message : String(decodeError)}`);
-            }
-            
-            // Log available utilities in nostrTools for debugging
-            console.log('📚 Available nostrTools methods:', Object.keys(nostrTools).join(', '));
-            
-            // Convert hex to Uint8Array manually
-            const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
-            
-            // Get server pubkey from the private key for display/logging
-            const serverPubkey = nostrTools.getPublicKey(privateKeyBytes);
-            const serverNpub = nostrTools.nip19.npubEncode(serverPubkey);
-            console.log(`Server pubkey: ${serverPubkey.substring(0, 8)}... (npub: ${serverNpub.substring(0, 8)}...)`);
-            
-            if (decryptionStatus) {
-              decryptionStatus.textContent = `Attempting decryption of key using server npub: ${serverNpub.substring(0, 8)}...`;
-            }
-            
-            // Attempt NIP-44 decryption with the server's private key
-            try {
-              console.log('🔓 Attempting NIP-44 decryption with server key');
-              console.log('📄 Encrypted key from event:', encryptedKey);
-              console.log('🔑 Using server key (first 8 bytes):', privateKeyHex.substring(0, 16));
-              
-              // Check if NIP-44 is available
-              const nip44Any = nostrTools.nip44 as any;
-              if (!nip44Any) {
-                console.error('❌ nostrTools.nip44 is undefined!');
-                console.log('📋 Available nostrTools modules:', Object.keys(nostrTools).join(', '));
-                throw new Error('NIP-44 module not available in nostr-tools');
-              }
-              
-              if (typeof nip44Any.decrypt !== 'function') {
-                console.error('❌ nostrTools.nip44.decrypt is not a function!');
-                console.log('📋 nip44 object properties:', Object.keys(nip44Any).join(', '));
-                throw new Error('NIP-44 decrypt function not available in nostr-tools');
-              }
-              
-              // Perform the decryption using only nostr-tools (no window.nostr!)
-              console.log('⏳ Calling nip44.decrypt with the correct parameters...');
-              console.log('📄 Ciphertext:', encryptedKey);
-              
-              console.log('🔑 Private key (hex):', privateKeyHex.substring(0, 16) + '...');
-              console.log('👤 Sender pubkey:', event.pubkey);
-              
-              // Calculate the shared secret (conversation key) between server private key and sender pubkey
-              // This is needed for NIP-44 decryption
-              const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
-              const conversationKey = nostrTools.nip44.getConversationKey(privateKeyBytes, event.pubkey);
-              
-              // Use the correct parameters for nip44.decrypt (payload, conversationKey)
-              decryptedKey = nip44Any.decrypt(encryptedKey, conversationKey);
-              console.log('✅ NIP-44 key decryption succeeded!');
-              console.log('🔑 Decrypted key:', decryptedKey.substring(0, 10) + '...');
-              
-              if (decryptionStatus) {
-                decryptionStatus.textContent = `Successfully decrypted key with server npub: ${serverNpub.substring(0, 8)}...`;
-                decryptionStatus.classList.add('success');
-              }
-            } catch (nip44Error) {
-              console.error('NIP-44 decryption error:', nip44Error);
-              if (decryptionStatus) {
-                decryptionStatus.textContent = `Decryption failed with server npub: ${serverNpub.substring(0, 8)}...`;
-                decryptionStatus.classList.add('error');
-              }
-              
-              // Fallback to using the encrypted key directly
-              console.warn('Falling back to using encrypted key directly');
-              decryptedKey = encryptedKey;
-            }
-            
-            // Now use the decrypted key to decrypt the content using Web Crypto API
-            try {
-              console.log('🔒 Attempting to decrypt content with Web Crypto API');
-              console.log('📄 Encrypted content (first 50 chars):', event.content.substring(0, 50) + '...');
-              console.log('🔑 Using decrypted key:', decryptedKey.substring(0, 10) + '...');
-              
-              // Check if the content is a valid Base64 string before attempting decryption
-              // This is important for Web Crypto API which expects properly formatted input
-              let contentToDecrypt = event.content;
-              
-              // Try to detect if the content is already in Base64 format
-              // If not, we need to encode it properly
-              try {
-                // Check if it's a valid base64 string by trying to decode it
-                atob(contentToDecrypt);
-                console.log('Content appears to be in Base64 format already');
-              // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
-              } catch (_) {
-                // If decoding fails, content is not in Base64 format
-                // Encode it properly for the crypto API
-                console.log('Content is not in Base64 format, converting...');
-                contentToDecrypt = btoa(contentToDecrypt);
-              }
-              
-              console.time('contentDecryption');
-              const decryptedEventContent = await this.httpService.decryptWithWebCrypto(
-                contentToDecrypt,
-                decryptedKey
-              );
-              console.timeEnd('contentDecryption');
-              
-              console.log('✅ Content decryption succeeded!');
-              console.log('📋 Decrypted content (first 100 chars):',
-                decryptedEventContent.substring(0, 100).replace(/\n/g, '\\n') + '...');
-              
-              decryptedContent = decryptedEventContent;
-              
-              if (decryptionStatus) {
-                decryptionStatus.textContent = `Decryption complete using server npub: ${serverNpub.substring(0, 8)}...`;
-                decryptionStatus.classList.add('success');
-              }
-            } catch (contentDecryptError) {
-              console.error('❌ Content decryption failed:', contentDecryptError);
-              console.error('📦 Error object:', JSON.stringify(contentDecryptError, Object.getOwnPropertyNames(contentDecryptError)));
-              console.log('🔑 Using decrypted key:', decryptedKey.substring(0, 10) + '...');
-              console.log('📄 Content format correct?', event.content.startsWith('Aes') ? 'Yes (starts with Aes)' : 'No');
-              
-              // Try direct decryption with NIP-44 as a fallback
-              try {
-                console.log('Trying direct content decryption with NIP-44 as fallback...');
-                const nip44Any = nostrTools.nip44 as any;
-                
-                // Calculate the shared secret (conversation key) between server private key and sender pubkey
-                // This is needed for NIP-44 decryption
-                const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
-                const conversationKey = nostrTools.nip44.getConversationKey(privateKeyBytes, event.pubkey);
-                
-                // Use the correct parameters for nip44.decrypt (payload, conversationKey)
-                const directDecryptedContent = nip44Any.decrypt(event.content, conversationKey);
-                console.log('Direct NIP-44 content decryption succeeded!');
-                decryptedContent = directDecryptedContent;
-                
-                if (decryptionStatus) {
-                  decryptionStatus.textContent = `Direct decryption succeeded with NIP-44`;
-                  decryptionStatus.classList.add('success');
-                }
-              } catch (directDecryptError) {
-                console.error('❌ Direct NIP-44 content decryption also failed:', directDecryptError);
-                
-                decryptedContent = `Key decryption successful, but content decryption failed: ${contentDecryptError instanceof Error ? contentDecryptError.message : String(contentDecryptError)}
-              
-Encrypted content: ${event.content.substring(0, 50)}...
-Decrypted key: ${decryptedKey}`;
-
-                if (decryptionStatus) {
-                  decryptionStatus.textContent = 'Content decryption failed (key decryption was successful)';
-                  decryptionStatus.classList.add('error');
-                }
-              }
-            }
-          } catch (error) {
-            console.error('Error during decryption process:', error);
-            decryptedKey = encryptedKey; // Fallback
-            decryptedContent = `[Decryption error: ${error instanceof Error ? error.message : String(error)}]\n${event.content}`;
-            
-            if (decryptionStatus) {
-              decryptionStatus.textContent = `Decryption error: ${error instanceof Error ? error.message : String(error)}`;
-              decryptionStatus.classList.add('error');
-            }
-          }
-        } catch (outerError) {
-          console.error("Outer decryption error:", outerError);
-          decryptedContent = `[NIP-44 decryption failed: ${outerError instanceof Error ? outerError.message : String(outerError)}]\n${event.content}`;
-        }
-      }
+      // Execute the HTTP request
+      const response = await this.httpClient.sendHttpRequest(httpContent);
       
-      // Update the event
-      receivedEvent.decrypted = true;
-      receivedEvent.decryptedContent = decryptedContent;
-      this.receivedEvents.set(eventId, receivedEvent);
-      
-      // Update UI if this event is currently being viewed
-      // Since there might be issues extracting the ID from the header text with regex,
-      // let's use a more direct approach to force the UI refresh
-      if (this.eventDetails) {
-        // Always refresh the UI to show the decrypted content
-        this.showEventDetails(eventId);
-        
-        // Explicitly update the http-content element to show the decrypted content
-        const httpContentElement = this.eventDetails.querySelector('#http-content .http-content');
-        if (httpContentElement) {
-          httpContentElement.textContent = decryptedContent;
-        }
-      }
+      // Show response dialog
+      this.showHttpResponseDialog(response, receivedEvent.event);
     } catch (error) {
-      console.error("Failed to process event:", error);
+      ToastNotifier.show(`Error: ${error instanceof Error ? error.message : String(error)}`, 'error');
     }
   }
-
+  
   /**
-   * Update relay status in UI
-   * @param message The status message
-   * @param className The CSS class name
+   * Show HTTP response in a dialog
+   * @param response The HTTP response content
+   * @param requestEvent The original request event
    */
-  public updateRelayStatus(message: string, className: string): void {
-    if (this.relayStatus) {
-      this.relayStatus.textContent = message;
-      this.relayStatus.className = `relay-status ${className}`;
-    }
-  }
-
-  /**
-   * Filter events in the UI based on the showAllEvents checkbox state
-   * @param showAllEvents Whether to show all events or only those for the server
-   */
-  public filterEventsInUI(showAllEvents: boolean): void {
-    if (!this.eventsList) {
-      return;
-    }
-
-    // Get all event items
-    const eventItems = this.eventsList.querySelectorAll('.event-item');
+  private showHttpResponseDialog(response: string, requestEvent: NostrEvent): void {
+    // Create modal dialog
+    const dialog = document.createElement('div');
+    dialog.className = 'http-response-dialog';
     
-    // Iterate through each event item
-    eventItems.forEach((item) => {
-      const isToServer = item.getAttribute('data-to-server') === 'true';
-      
-      if (showAllEvents) {
-        // Show all events
-        (item as HTMLElement).style.display = '';
-      } else {
-        // Only show events addressed to the server
-        (item as HTMLElement).style.display = isToServer ? '' : 'none';
+    dialog.innerHTML = `
+      <div class="dialog-container">
+        <div class="dialog-header">
+          <h3>HTTP Response</h3>
+          <button class="close-dialog">&times;</button>
+        </div>
+        <div class="dialog-content">
+          <div class="tab-buttons">
+            <button class="tab-btn" data-tab="raw">Raw</button>
+            <button class="tab-btn active" data-tab="formatted">Formatted</button>
+          </div>
+          <div class="tab-content" id="raw">
+            <pre>${response}</pre>
+          </div>
+          <div class="tab-content active" id="formatted">
+            <div class="formatted-container">
+              ${HttpFormatter.formatHttpContent(response, false, true)}
+            </div>
+          </div>
+        </div>
+        <div class="dialog-actions">
+          <button class="create-21121-btn">Create 21121 Response Event</button>
+        </div>
+      </div>
+    `;
+    
+    document.body.appendChild(dialog);
+    
+    // Set up tab switching
+    const tabButtons = dialog.querySelectorAll('.tab-btn');
+    tabButtons.forEach(button => {
+      button.addEventListener('click', () => {
+        // Remove active class from all buttons and tabs
+        tabButtons.forEach(btn => btn.classList.remove('active'));
+        dialog.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
+        
+        // Add active class to clicked button
+        button.classList.add('active');
+        
+        // Show corresponding tab
+        const tabId = (button as HTMLElement).dataset.tab;
+        if (tabId) {
+          const tab = dialog.querySelector(`#${tabId}`);
+          if (tab) tab.classList.add('active');
+        }
+      });
+    });
+    
+    // Close dialog
+    const closeBtn = dialog.querySelector('.close-dialog');
+    if (closeBtn) {
+      closeBtn.addEventListener('click', () => {
+        document.body.removeChild(dialog);
+      });
+    }
+    
+    // Create 21121 response event
+    const createBtn = dialog.querySelector('.create-21121-btn');
+    if (createBtn) {
+      createBtn.addEventListener('click', async () => {
+        const success = await this.responseHandler.createAndPublish21121Response(requestEvent, response);
+        if (success) {
+          document.body.removeChild(dialog);
+        }
+      });
+    }
+  }
+  
+  /**
+   * Show dialog to create a response
+   * @param requestEvent The request event to create a response for
+   */
+  private createResponseDialog(requestEvent: NostrEvent): void {
+    const dialog = document.createElement('div');
+    dialog.className = 'create-response-dialog';
+    
+    dialog.innerHTML = `
+      <div class="dialog-container">
+        <div class="dialog-header">
+          <h3>Create HTTP Response</h3>
+          <button class="close-dialog">&times;</button>
+        </div>
+        <div class="dialog-content">
+          <p>Enter HTTP response content:</p>
+          <textarea rows="10" placeholder="HTTP/1.1 200 OK
+Content-Type: application/json
+
+{
+  'status': 'success'
+}"></textarea>
+        </div>
+        <div class="dialog-actions">
+          <button class="cancel-btn">Cancel</button>
+          <button class="create-btn">Create Response</button>
+        </div>
+      </div>
+    `;
+    
+    document.body.appendChild(dialog);
+    
+    // Close dialog handlers
+    const closeBtn = dialog.querySelector('.close-dialog');
+    const cancelBtn = dialog.querySelector('.cancel-btn');
+    [closeBtn, cancelBtn].forEach(btn => {
+      if (btn) {
+        btn.addEventListener('click', () => {
+          document.body.removeChild(dialog);
+        });
       }
     });
+    
+    // Create response
+    const createBtn = dialog.querySelector('.create-btn');
+    const textarea = dialog.querySelector('textarea');
+    if (createBtn && textarea) {
+      createBtn.addEventListener('click', async () => {
+        const responseContent = textarea.value.trim();
+        if (!responseContent) {
+          ToastNotifier.show('Response content cannot be empty', 'error');
+          return;
+        }
+        
+        const success = await this.responseHandler.createAndPublish21121Response(requestEvent, responseContent);
+        if (success) {
+          document.body.removeChild(dialog);
+        }
+      });
+    }
   }
 }
diff --git a/client/src/services/UiService.ts.backup b/client/src/services/UiService.ts.backup
new file mode 100644
index 0000000..de85612
--- /dev/null
+++ b/client/src/services/UiService.ts.backup
@@ -0,0 +1,1391 @@
+/**
+ * UiService.ts
+ * Handles UI-related operations and DOM manipulation
+ */
+
+import jsQR from 'jsqr';
+import * as nostrTools from 'nostr-tools';
+
+import type { NostrEvent } from '../relay';
+
+import type { HttpService } from './HttpService';
+import type { ProfileData } from './NostrCacheService';
+import type { ReceivedEvent } from './NostrEventService';
+import { NostrService } from './NostrService';
+import type { NostrService } from './NostrService';
+
+/**
+ * Class for managing UI operations
+ */
+export class UiService {
+  private nostrService: NostrService;
+  private relatedEvents: Map<string, string[]> = new Map<string, string[]>(); // Map of event IDs to related event IDs
+  private httpService: HttpService;
+  private receivedEvents = new Map<string, ReceivedEvent>();
+  
+  // DOM Elements
+  private relayStatus: HTMLElement | null = null;
+  private eventsList: HTMLElement | null = null;
+  private eventDetails: HTMLElement | null = null;
+  
+  /**
+   * Constructor
+   * @param nostrService An instance of NostrService
+   * @param httpService An instance of HttpService
+   */
+  constructor(nostrService: NostrService, httpService: HttpService) {
+    this.nostrService = nostrService;
+    this.httpService = httpService;
+  }
+
+  /**
+   * Initialize the UI elements and event listeners
+   */
+  public initialize(): void {
+    // Get DOM elements
+    this.relayStatus = document.getElementById('relayStatus');
+    this.eventsList = document.getElementById('eventsList');
+    this.eventDetails = document.getElementById('eventDetails');
+    
+    // Initialize event handlers
+    this.setupTabNavigation();
+    this.setupQRScanner();
+    this.setupRawTextInput();
+    this.setupEventHandlers();
+    
+    // Set event handler for Nostr events
+    this.nostrService.setEventHandler((event) => this.processEvent(event));
+    
+    // Apply initial filtering based on checkbox state
+    setTimeout(() => {
+      const showAllEventsCheckbox = document.getElementById('showAllEvents') as HTMLInputElement;
+      if (showAllEventsCheckbox) {
+        this.filterEventsInUI(showAllEventsCheckbox.checked);
+      }
+    }, 500); // Short delay to ensure UI is fully initialized
+  }
+
+  /**
+   * Set up tab navigation for the UI
+   */
+  private setupTabNavigation(): void {
+    // For event detail tabs which use .tab-btn class
+    const detailTabs = document.querySelectorAll('.tab-content');
+    const detailTabButtons = document.querySelectorAll('.tab-btn');
+    
+    // Set up event detail tab navigation
+    if (detailTabButtons.length > 0) {
+      detailTabButtons.forEach(button => {
+        button.addEventListener('click', () => {
+          // Remove active class from all buttons and tabs
+          detailTabButtons.forEach(b => b.classList.remove('active'));
+          detailTabs.forEach(tab => tab.classList.remove('active'));
+          
+          // Add active class to clicked button and corresponding tab
+          button.classList.add('active');
+          const tabId = button.getAttribute('data-tab');
+          
+          if (tabId) {
+            const activeTab = document.getElementById(tabId);
+            if (activeTab) {
+              activeTab.classList.add('active');
+            }
+          }
+        });
+      });
+    }
+    
+    // For main page tabs which use .tab-button class
+    const mainTabButtons = document.querySelectorAll('.tab-button');
+    
+    mainTabButtons.forEach(button => {
+      button.addEventListener('click', () => {
+        // Remove active class from all tab buttons
+        mainTabButtons.forEach(b => b.classList.remove('active'));
+        
+        // Add active class to clicked button
+        button.classList.add('active');
+        
+        // Get the section ID from data-tab attribute
+        const sectionId = button.getAttribute('data-tab');
+        
+        if (sectionId) {
+          // Hide all section content
+          const sections = document.querySelectorAll('#relay-connection-section, #qr-code-scanner-section, #raw-event-input-section');
+          sections.forEach(section => section.classList.remove('active'));
+          
+          // Show the selected section
+          const activeSection = document.getElementById(sectionId);
+          if (activeSection) {
+            activeSection.classList.add('active');
+          }
+        }
+      });
+    });
+  }
+
+  /**
+   * Set up QR scanner functionality
+   */
+  private setupQRScanner(): void {
+    const scannerContainer = document.getElementById('qrScannerContainer');
+    const startScanBtn = document.getElementById('startScanBtn');
+    const stopScanBtn = document.getElementById('stopScanBtn');
+    const videoElement = document.getElementById('qrVideo') as HTMLVideoElement;
+    const qrResultInput = document.getElementById('qrResultInput') as HTMLTextAreaElement;
+    
+    if (!scannerContainer || !startScanBtn || !stopScanBtn || !videoElement || !qrResultInput) {
+      return;
+    }
+    
+    // Start QR scanner
+    async function startQRScanner(): Promise<void> {
+      try {
+        if (scannerContainer && startScanBtn && stopScanBtn) {
+          scannerContainer.classList.remove('hidden');
+          startScanBtn.classList.add('hidden');
+          stopScanBtn.classList.remove('hidden');
+        }
+        
+        // Get user media
+        const stream = await navigator.mediaDevices.getUserMedia({
+          video: { facingMode: 'environment' }
+        });
+        
+        videoElement.srcObject = stream;
+        videoElement.play();
+        
+        // Start scanning for QR codes
+        scanQRCode();
+      } catch (error) {
+        alert(`Error accessing camera: ${error instanceof Error ? error.message : String(error)}`);
+        stopQRScanner();
+      }
+    }
+    
+    // Stop QR scanner
+    function stopQRScanner(): void {
+      if (scannerContainer && startScanBtn && stopScanBtn) {
+        scannerContainer.classList.add('hidden');
+        startScanBtn.classList.remove('hidden');
+        stopScanBtn.classList.add('hidden');
+      }
+      
+      // Stop the video stream
+      if (videoElement.srcObject) {
+        const stream = videoElement.srcObject as MediaStream;
+        const tracks = stream.getTracks();
+        
+        tracks.forEach(track => {
+          track.stop();
+        });
+        
+        videoElement.srcObject = null;
+      }
+    }
+    
+    // Add event listeners
+    startScanBtn.addEventListener('click', () => {
+      startQRScanner().catch(error => {
+        alert(`Error starting QR scanner: ${error instanceof Error ? error.message : String(error)}`);
+      });
+    });
+    
+    stopScanBtn.addEventListener('click', stopQRScanner);
+    
+    // Function to scan QR code from video feed
+    function scanQRCode(): void {
+      if (videoElement.readyState !== videoElement.HAVE_ENOUGH_DATA) {
+        // Not enough data yet, retry after a short delay
+        setTimeout(scanQRCode, 100);
+        return;
+      }
+      
+      const canvas = document.createElement('canvas');
+      const width = videoElement.videoWidth;
+      const height = videoElement.videoHeight;
+      
+      canvas.width = width;
+      canvas.height = height;
+      
+      const ctx = canvas.getContext('2d');
+      if (!ctx) {
+        setTimeout(scanQRCode, 100);
+        return;
+      }
+      
+      ctx.drawImage(videoElement, 0, 0, width, height);
+      const imageData = ctx.getImageData(0, 0, width, height);
+      
+      // Use jsQR to detect QR code
+      const code = jsQR(imageData.data, width, height, {
+        inversionAttempts: 'dontInvert'
+      });
+      
+      if (code) {
+        // QR code detected
+        qrResultInput.value = code.data;
+        stopQRScanner();
+      } else {
+        // No QR code found, retry after a short delay
+        setTimeout(scanQRCode, 100);
+      }
+    }
+  }
+
+  /**
+   * Set up raw text input functionality
+   */
+  private setupRawTextInput(): void {
+    const rawEventInput = document.getElementById('rawEventInput') as HTMLTextAreaElement;
+    const parseRawEventBtn = document.getElementById('parseRawEventBtn');
+    
+    if (!rawEventInput || !parseRawEventBtn) {
+      return;
+    }
+    
+    parseRawEventBtn.addEventListener('click', () => {
+      try {
+        const rawText = rawEventInput.value.trim();
+        if (!rawText) {
+          alert('Please enter a raw Nostr event.');
+          return;
+        }
+        
+        // Try to parse as JSON
+        const eventData = JSON.parse(rawText);
+        
+        // Validate as a Nostr event
+        if (!eventData.id || !eventData.pubkey || !eventData.created_at || 
+            !eventData.kind || !Array.isArray(eventData.tags) || 
+            typeof eventData.content !== 'string' || !eventData.sig) {
+          alert('Invalid Nostr event format.');
+          return;
+        }
+        
+        // Process the event
+        this.processEvent(eventData as NostrEvent);
+        
+        // Clear the input
+        rawEventInput.value = '';
+        
+        // Show notification
+        alert('Event processed successfully!');
+      } catch (error) {
+        alert(`Error parsing event: ${error instanceof Error ? error.message : String(error)}`);
+      }
+    });
+  }
+
+  /**
+   * Set up event handlers for the UI
+   */
+  private setupEventHandlers(): void {
+    // Connect to relay button
+    const connectRelayBtn = document.getElementById('connectRelayBtn');
+    const relayUrlInput = document.getElementById('relayUrlInput') as HTMLInputElement;
+    
+    if (connectRelayBtn && relayUrlInput) {
+      connectRelayBtn.addEventListener('click', async () => {
+        const relayUrl = relayUrlInput.value.trim();
+        if (!relayUrl) {
+          alert('Please enter a relay URL');
+          return;
+        }
+        
+        try {
+          // Connect to the relay
+          const success = await this.nostrService.connectToRelay(relayUrl);
+          
+          if (success) {
+            // Subscribe to events
+            const showAllEventsCheckbox = document.getElementById('showAllEvents') as HTMLInputElement;
+            const showAllEvents = showAllEventsCheckbox ? showAllEventsCheckbox.checked : false;
+            
+            const filter = this.nostrService.createKind21120Filter(showAllEvents);
+            await this.nostrService.subscribeToEvents(filter);
+          }
+        } catch (error) {
+          alert(`Error connecting to relay: ${error instanceof Error ? error.message : String(error)}`);
+        }
+      });
+    }
+    
+    // Show all events checkbox
+    const showAllEventsCheckbox = document.getElementById('showAllEvents') as HTMLInputElement;
+    if (showAllEventsCheckbox) {
+      showAllEventsCheckbox.addEventListener('change', async () => {
+        try {
+          // Resubscribe with new filter
+          const filter = this.nostrService.createKind21120Filter(showAllEventsCheckbox.checked);
+          await this.nostrService.subscribeToEvents(filter);
+          
+          // Also filter existing events in the UI
+          this.filterEventsInUI(showAllEventsCheckbox.checked);
+        } catch (error) {
+          alert(`Error updating subscription: ${error instanceof Error ? error.message : String(error)}`);
+        }
+      });
+    }
+    
+    // Connect to default relay on page load
+    setTimeout(async () => {
+      if (relayUrlInput && relayUrlInput.value && connectRelayBtn) {
+        // Trigger click on the connect button
+        connectRelayBtn.click();
+      }
+    }, 500);
+  }
+
+  /**
+   * Process a received Nostr event
+   * @param event The Nostr event to process
+   */
+  private processEvent(event: NostrEvent): void {
+    // Ensure event has an ID
+    if (!event.id) {
+      return;
+    }
+    
+    // Check if this is a new event
+    if (this.receivedEvents.has(event.id)) {
+      return;
+    }
+    
+    // Store the event
+    const receivedEvent: ReceivedEvent = {
+      id: event.id,
+      event: event,
+      receivedAt: Date.now(),
+      decrypted: false
+    };
+    
+    this.receivedEvents.set(event.id, receivedEvent);
+    
+    // Add event to UI
+    this.addEventToUI(receivedEvent);
+  }
+
+  /**
+   * Add an event to the UI
+   * @param receivedEvent The received event to add to the UI
+   */
+  private addEventToUI(receivedEvent: ReceivedEvent): void {
+    if (!this.eventsList) {
+      return;
+    }
+    
+    const event = receivedEvent.event;
+    
+    // Create event item with improved styling
+    const eventItem = document.createElement('div');
+    eventItem.className = 'event-item';
+    eventItem.dataset.id = event.id;
+    
+    // Get event ID for display
+    const eventIdForDisplay = event.id ? event.id.substring(0, 8) : 'unknown';
+    
+    // Determine if it's a request or response
+    const hasP = event.tags.some(tag => tag[0] === 'p');
+    const hasE = event.tags.some(tag => tag[0] === 'e');
+    const eventType = hasP ? 'HTTP Request' : (hasE ? 'HTTP Response' : 'Unknown');
+    
+    // Find recipient if available and check if it's addressed to our server
+    let recipient = '';
+    let isToServer = false;
+    const pTag = event.tags.find(tag => tag[0] === 'p');
+    
+    if (pTag && pTag.length > 1) {
+      recipient = `| To: ${pTag[1].substring(0, 8)}...`;
+      
+      // Check if this message is addressed to our server
+      try {
+        // Get the server pubkey from localStorage
+        const serverNsec = localStorage.getItem('serverNsec');
+        if (serverNsec) {
+          const decoded = nostrTools.nip19.decode(serverNsec);
+          if (decoded.type === 'nsec') {
+            // Convert Uint8Array to hex string
+            const privateKeyHex = Array.from(decoded.data as Uint8Array)
+              .map(b => b.toString(16).padStart(2, '0'))
+              .join('');
+            const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
+            const serverPubkey = nostrTools.getPublicKey(privateKeyBytes);
+            
+            // Check if the p tag matches our server pubkey
+            isToServer = (pTag[1] === serverPubkey);
+          }
+        }
+      } catch {
+        // Ignore errors when checking
+      }
+    }
+    
+    // Format the event item
+    eventItem.innerHTML = `
+      <div class="event-item-container">
+        <div class="event-avatar" data-pubkey="${event.pubkey}">
+          <div class="avatar-placeholder">👤</div>
+        </div>
+        <div class="event-content-wrapper">
+          <div class="event-header">
+            <div class="event-type ${eventType === 'HTTP Request' ? 'request' : 'response'}">${eventType}</div>
+            <div class="event-time">${new Date(event.created_at * 1000).toLocaleTimeString()}</div>
+          </div>
+          <div class="event-id">ID: ${eventIdForDisplay}... ${recipient} ${isToServer ? '<span class="server-match">✓</span>' : ''}</div>
+          <div class="event-pubkey">From: ${event.pubkey.substring(0, 8)}...</div>
+        </div>
+      </div>
+    `;
+    
+    // Set a data attribute to indicate if this event is addressed to our server
+    eventItem.dataset.toServer = isToServer.toString();
+    
+    // Add to list at the top
+    if (this.eventsList.firstChild) {
+      this.eventsList.insertBefore(eventItem, this.eventsList.firstChild);
+    } else {
+      this.eventsList.appendChild(eventItem);
+    }
+    
+    // Fetch profile for avatar after a short delay
+    const avatarElement = eventItem.querySelector('.event-avatar') as HTMLElement;
+    if (avatarElement) {
+      setTimeout(() => {
+        this.nostrService.fetchProfileData(event.pubkey)
+          .then(profile => this.updateAvatarElement(avatarElement, profile))
+          .catch(() => this.updateAvatarElement(avatarElement, null));
+      }, 10);
+    }
+    
+    // Add click event to show details
+    eventItem.addEventListener('click', () => {
+      if (event.id) {
+        this.showEventDetails(event.id);
+      }
+    });
+  }
+
+  /**
+   * Update an avatar element with profile data
+   * @param element The element to update
+   * @param profile The profile data to use
+   */
+  private updateAvatarElement(element: HTMLElement, profile: ProfileData | null): void {
+    if (!element) {
+      return;
+    }
+    
+    if (profile && profile.picture) {
+      // Use the profile picture URL
+      element.innerHTML = `<img src="${profile.picture}" alt="Avatar" class="avatar-img" />`;
+    } else {
+      // Use default avatar
+      element.innerHTML = `<div class="avatar-placeholder">👤</div>`;
+    }
+    
+    // Make sure it's visible
+    element.style.opacity = '1';
+  }
+
+  /**
+   * Show event details in the UI
+   * @param eventId The ID of the event to show details for
+   */
+  public showEventDetails(eventId: string): void {
+    if (!this.eventDetails) {
+      console.error('Event details element not found!');
+      return;
+    }
+    
+    const receivedEvent = this.receivedEvents.get(eventId);
+    if (!receivedEvent) {
+      console.error(`Event with ID ${eventId} not found in receivedEvents Map!`);
+      return;
+    }
+    
+    const event = receivedEvent.event;
+    
+    // Ensure event has an ID (should already be verified)
+    const eventIdForDisplay = event.id ? event.id.substring(0, 8) : 'unknown';
+    
+    // Determine if this is a request or response
+    const isRequest = event.tags.some(tag => tag[0] === 'p');
+    const isResponse = event.tags.some(tag => tag[0] === 'e');
+    let eventTypeLabel = isRequest ? 'HTTP Request' : (isResponse ? 'HTTP Response' : 'Unknown Type');
+    
+    // Check if this is a 21121 event (HTTP response)
+    const is21121Event = event.kind === 21121;
+    if (is21121Event) {
+      eventTypeLabel = 'HTTP Response (21121)';
+    }
+    
+    // Get the decrypted or original content
+    const httpContent = receivedEvent.decrypted ?
+      (receivedEvent.decryptedContent || event.content) :
+      event.content;
+    
+    // Create raw JSON representation of the event
+    const rawJson = JSON.stringify(event, null, 2);
+    
+    // Display the event details with a tabbed interface
+    this.eventDetails.innerHTML = `
+      <div class="event-detail-header">
+        <h3>${eventTypeLabel} (ID: ${eventIdForDisplay}...)</h3>
+        <div class="event-timestamp">${new Date(event.created_at * 1000).toLocaleString()}</div>
+      </div>
+      
+      <div class="event-detail-tabs">
+        <button class="tab-btn" data-tab="raw-json">Raw JSON</button>
+        <button class="tab-btn" data-tab="raw-http">Raw HTTP</button>
+        <button class="tab-btn active" data-tab="formatted-http">Formatted HTTP</button>
+      </div>
+      
+      <div class="event-detail-content">
+        <div class="tab-content" id="raw-json">
+          <pre class="json-content">${rawJson}</pre>
+        </div>
+        <div class="tab-content" id="raw-http">
+          ${isRequest ?
+            `<div class="http-content-header">
+              <button class="execute-http-request-btn">Execute HTTP Request</button>
+            </div>` :
+            ''
+          }
+          <pre class="http-content">${httpContent}</pre>
+          ${!receivedEvent.decrypted ? '<div class="decryption-status" id="decryption-status-' + eventId + '">Attempting decryption...</div>' : ''}
+        </div>
+        <div class="tab-content active" id="formatted-http">
+          ${isRequest ?
+            `<div class="http-content-header">
+              <button class="execute-http-request-btn">Execute HTTP Request</button>
+            </div>` :
+            ''
+          }
+          <div class="http-formatted-container">
+            ${this.formatHttpContent(httpContent, isRequest, isResponse || is21121Event)}
+          </div>
+          ${!receivedEvent.decrypted ? '<div class="decryption-status" id="decryption-status-' + eventId + '">Attempting decryption...</div>' : ''}
+        </div>
+      </div>
+    `;
+    
+    // If the event isn't decrypted yet, trigger decryption
+    if (!receivedEvent.decrypted && event.id) {
+      const eventId = event.id; // Store in a const to make TypeScript happy
+      console.log(`Triggering decryption for event ${eventId.substring(0, 8)}...`);
+      // Use setTimeout to allow the UI to render first
+      setTimeout(() => {
+        this.decryptEvent(eventId).catch(error => {
+          console.error(`Error in decryption process:`, error);
+          const decryptionStatus = document.getElementById(`decryption-status-${eventId}`);
+          if (decryptionStatus) {
+            decryptionStatus.textContent = `Decryption error: ${error.message || 'Unknown error'}`;
+            decryptionStatus.classList.add('error');
+          }
+        });
+      }, 100);
+    }
+    
+    // Add event listeners for tab buttons
+    const tabButtons = this.eventDetails.querySelectorAll('.tab-btn');
+    tabButtons.forEach(button => {
+      button.addEventListener('click', () => {
+        // Get the target tab
+        const targetTabId = (button as HTMLElement).dataset.tab;
+        if (!targetTabId) {
+          return;
+        }
+        
+        // Remove active class from all buttons and content sections
+        tabButtons.forEach(btn => btn.classList.remove('active'));
+        // Use non-null assertion since we already checked eventDetails isn't null at the beginning
+        this.eventDetails!.querySelectorAll('.tab-content').forEach(section => section.classList.remove('active'));
+        
+        // Add active class to clicked button and corresponding content section
+        button.classList.add('active');
+        const targetSection = document.getElementById(targetTabId);
+        if (targetSection) {
+          targetSection.classList.add('active');
+        }
+      });
+    });
+    
+    // Add event listener for the execute HTTP request button if it exists
+    const executeButton = this.eventDetails.querySelectorAll('.execute-http-request-btn');
+    if (executeButton.length > 0 && isRequest) {
+      executeButton.forEach(button => {
+        button.addEventListener('click', async () => {
+          // Change button state to indicate processing
+          const originalText = button.textContent || 'Execute HTTP Request';
+          button.textContent = 'Executing...';
+          button.setAttribute('disabled', 'true');
+          
+          try {
+            // Execute the HTTP request
+            const response = await this.httpService.executeHttpRequest(httpContent);
+            
+            // Display the response
+            this.displayHttpResponse(response);
+            
+            // Ask if the user wants to create and publish a 21121 response event
+            if (confirm('Do you want to create and publish a NIP-21121 response event?')) {
+              await this.create21121ResponseEvent(event, response);
+            }
+          } catch (error) {
+            // Display error in a modal
+            this.displayHttpResponse(`Error executing request: ${error instanceof Error ? error.message : String(error)}`);
+          } finally {
+            // Restore button state
+            button.textContent = originalText;
+            button.removeAttribute('disabled');
+          }
+        });
+      });
+    }
+    
+    // If this is a 21120 event, check if there are any 21121 responses
+    if (event.kind === 21120 && event.id) {
+      this.checkFor21121Responses(event.id);
+    }
+  }
+
+  /**
+   * Display an HTTP response in a modal
+   * @param responseText The response text to display
+   */
+  private displayHttpResponse(responseText: string): void {
+    // Create a modal dialog element
+    const modal = document.createElement('div');
+    modal.className = 'http-response-modal';
+    
+    // Add the response content
+    modal.innerHTML = `
+      <div class="http-response-container">
+        <div class="http-response-header">
+          <h3>HTTP Response</h3>
+          <button class="close-modal-btn">&times;</button>
+        </div>
+        <div class="http-response-content">
+          <pre>${responseText}</pre>
+        </div>
+      </div>
+    `;
+    
+    // Add the modal to the DOM
+    document.body.appendChild(modal);
+    
+    // Add event listener to close the modal
+    const closeBtn = modal.querySelector('.close-modal-btn');
+    if (closeBtn) {
+      closeBtn.addEventListener('click', () => {
+        document.body.removeChild(modal);
+      });
+    }
+    
+    // Also close the modal when clicking outside the content
+    modal.addEventListener('click', (event) => {
+      if (event.target === modal) {
+        document.body.removeChild(modal);
+      }
+    });
+  }
+
+  /**
+   * Attempt to decrypt an event using NIP-44
+   * @param eventId The ID of the event to decrypt
+   */
+  public async decryptEvent(eventId: string): Promise<void> {
+    console.log(`==== DECRYPTION PROCESS STARTED FOR EVENT ${eventId.substring(0, 8)}... ====`);
+    
+    // Check if we have the event
+    const receivedEvent = this.receivedEvents.get(eventId);
+    if (!receivedEvent) {
+      console.error(`❌ Event with ID ${eventId.substring(0, 8)}... not found in receivedEvents Map!`);
+      return;
+    }
+    
+    // Check if already decrypted
+    if (receivedEvent.decrypted) {
+      console.log(`ℹ️ Event ${eventId.substring(0, 8)}... is already decrypted, skipping`);
+      return;
+    }
+    
+    // Check if this event is addressed to our server by finding p tag
+    const event = receivedEvent.event;
+    console.log(`=== Checking if event ${eventId.substring(0, 8)}... is addressed to this server ===`);
+    const pTag = event.tags.find(tag => tag[0] === 'p');
+    console.log(`p tag found: ${pTag ? 'yes' : 'no'}`);
+    let isAddressedToServer = false;
+    
+    if (pTag && pTag.length > 1) {
+      console.log(`p tag value: ${pTag[1].substring(0, 8)}...`);
+      
+      // Get the server pubkey from localStorage
+      const serverNsec = localStorage.getItem('serverNsec');
+      console.log(`Server private key in localStorage: ${serverNsec ? 'yes' : 'no'}`);
+      
+      if (serverNsec) {
+        try {
+          const decoded = nostrTools.nip19.decode(serverNsec);
+          if (decoded.type === 'nsec') {
+            // For nostr-tools, we need to work with the raw private key bytes
+            // In practice, the decoded.data from nsec should always be a Uint8Array
+            const serverPrivateKeyBytes = decoded.data as Uint8Array;
+            
+            // Get server pubkey from private key bytes
+            const serverPubkey = nostrTools.getPublicKey(serverPrivateKeyBytes);
+            console.log(`Server pubkey: ${serverPubkey.substring(0, 8)}...`);
+            
+            // Check if the p tag matches our server pubkey
+            isAddressedToServer = (pTag[1] === serverPubkey);
+            console.log(`Message addressed to this server: ${isAddressedToServer ? 'yes ✓' : 'no ✗'}`);
+            
+            // Update UI to show the correct server pubkey
+            const serverNpubInput = document.getElementById('serverNpub') as HTMLInputElement;
+            if (serverNpubInput) {
+              const serverNpub = nostrTools.nip19.npubEncode(serverPubkey);
+              // Only update if needed
+              if (serverNpubInput.value !== serverNpub && serverNpubInput.value !== serverPubkey) {
+                console.log(`Updating server pubkey display in UI`);
+                serverNpubInput.value = serverNpub;
+                // Update format indicator
+                const formatIndicator = document.getElementById('formatIndicator');
+                if (formatIndicator) {
+                  formatIndicator.textContent = 'Currently showing: NPUB format';
+                }
+              }
+            }
+          }
+        } catch (error) {
+          console.error('Error checking if event is addressed to server:', error);
+        }
+      }
+    }
+    
+    // If not addressed to our server, mark as "decrypted" but with a note
+    if (!isAddressedToServer) {
+      console.log(`⚠️ Event ${eventId.substring(0, 8)}... is not addressed to this server, skipping decryption`);
+      receivedEvent.decrypted = true;
+      receivedEvent.decryptedContent = "[This message is not addressed to this server]";
+      this.receivedEvents.set(eventId, receivedEvent);
+      
+      // Update UI
+      const decryptionStatus = document.getElementById(`decryption-status-${eventId}`);
+      if (decryptionStatus) {
+        decryptionStatus.textContent = 'Not addressed to this server';
+        decryptionStatus.classList.add('info');
+      }
+      
+      // Update UI if this event is currently being viewed
+      const selectedEventId = this.eventDetails?.querySelector('h3')?.textContent?.match(/(.+)\.\.\./)?.[1];
+      if (selectedEventId && `${selectedEventId}...` === `${eventId.substring(0, 8)}...`) {
+        this.showEventDetails(eventId);
+      }
+      
+      return;
+    }
+    
+    try {
+      console.log(`✓ Found event ${eventId.substring(0, 8)}..., proceeding with decryption`);
+      const event = receivedEvent.event;
+      let decryptedContent: string;
+      
+      // Look for a "key" tag in the event
+      const keyTag = event.tags.find(tag => tag[0] === 'key');
+      
+      if (!keyTag || keyTag.length < 2) {
+        decryptedContent = `[No key tag found for decryption]\n${event.content}`;
+      } else {
+        try {
+          // Extract the encrypted key from the tag
+          const encryptedKey = keyTag[1];
+          
+          console.log(`Starting decryption process for event ${eventId.substring(0, 8)}...`);
+          
+          // Declare variable to hold the decrypted key
+          let decryptedKey: string;
+          
+          // Update the decryption status UI
+          const decryptionStatus = document.getElementById(`decryption-status-${eventId}`);
+          if (decryptionStatus) {
+            decryptionStatus.textContent = 'Getting server private key from storage...';
+          }
+          // Get the server private key from localStorage
+          console.log('🔑 Looking for server nsec in localStorage...');
+          const serverNsec = localStorage.getItem('serverNsec');
+          if (!serverNsec) {
+            console.error('❌ Server private key (nsec) not found in localStorage');
+            console.log('💡 This should be set when server is registered in NostrService.createOrUpdate31120Event');
+            
+            // Check all localStorage keys to help debug
+            console.log('📋 Available localStorage keys:');
+            for (let i = 0; i < localStorage.length; i++) {
+              const key = localStorage.key(i);
+              if (key) {
+                console.log(`   - ${key}: ${localStorage.getItem(key)?.substring(0, 20)}...`);
+              }
+            }
+            
+            if (decryptionStatus) {
+              decryptionStatus.textContent = 'Server private key (nsec) not found in localStorage';
+              decryptionStatus.classList.add('error');
+            }
+            throw new Error('Server private key (nsec) not found in localStorage');
+          }
+          
+          console.log('✓ Server nsec found in localStorage:', serverNsec.substring(0, 10) + '...');
+          
+          
+          try {
+            // Make sure nostr-tools is available
+            if (!nostrTools || !nostrTools.nip19 || !nostrTools.nip44) {
+              console.error('Required nostr-tools libraries not available');
+              if (decryptionStatus) {
+                decryptionStatus.textContent = 'Required nostr-tools libraries not available';
+                decryptionStatus.classList.add('error');
+              }
+              throw new Error('Required nostr-tools libraries not available');
+            }
+            
+            console.log('Decoding nsec to get the raw private key');
+            
+            // Let's try a completely different approach to extract the private key from nsec
+            console.log('🔑 Extracting private key from server nsec using a different method');
+            
+            let privateKeyHex: string;
+            
+            try {
+              // Example shown in the documentation uses a direct hex string
+              // The nsec format should decode to the raw private key
+              const decoded = nostrTools.nip19.decode(serverNsec);
+              
+              if (decoded.type !== 'nsec') {
+                throw new Error(`Expected nsec but got ${decoded.type}`);
+              }
+              
+              // Log the raw data for debugging
+              console.log('Raw decoded data type:', typeof decoded.data);
+              console.log('Raw decoded data length:', (decoded.data as Uint8Array).length);
+              
+              // Direct conversion to hex
+              // Convert Uint8Array to hex string
+              privateKeyHex = Array.from(decoded.data as Uint8Array)
+                .map(b => b.toString(16).padStart(2, '0'))
+                .join('');
+              console.log('✅ Successfully decoded private key from buffer:', privateKeyHex.substring(0, 8) + '...');
+              // Check if the private key looks valid (should be 64 hex chars)
+              if (privateKeyHex.length !== 64) {
+                console.warn(`⚠️ Private key has unexpected length: ${privateKeyHex.length} (expected 64)`);
+              }
+              
+              // Check if the private key looks valid (should be 64 hex chars)
+              if (privateKeyHex.length !== 64) {
+                console.warn(`⚠️ Private key has unexpected length: ${privateKeyHex.length} (expected 64)`);
+              }
+            } catch (decodeError: unknown) {
+              console.error('❌ Failed to decode nsec:', decodeError);
+              if (decryptionStatus) {
+                decryptionStatus.textContent = 'Failed to decode server key';
+                decryptionStatus.classList.add('error');
+              }
+              throw new Error(`Failed to decode server key: ${decodeError instanceof Error ? decodeError.message : String(decodeError)}`);
+            }
+            
+            // Log available utilities in nostrTools for debugging
+            console.log('📚 Available nostrTools methods:', Object.keys(nostrTools).join(', '));
+            
+            // Convert hex to Uint8Array manually
+            const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
+            
+            // Get server pubkey from the private key for display/logging
+            const serverPubkey = nostrTools.getPublicKey(privateKeyBytes);
+            const serverNpub = nostrTools.nip19.npubEncode(serverPubkey);
+            console.log(`Server pubkey: ${serverPubkey.substring(0, 8)}... (npub: ${serverNpub.substring(0, 8)}...)`);
+            
+            if (decryptionStatus) {
+              decryptionStatus.textContent = `Attempting decryption of key using server npub: ${serverNpub.substring(0, 8)}...`;
+            }
+            
+            // Attempt NIP-44 decryption with the server's private key
+            try {
+              console.log('🔓 Attempting NIP-44 decryption with server key');
+              console.log('📄 Encrypted key from event:', encryptedKey);
+              console.log('🔑 Using server key (first 8 bytes):', privateKeyHex.substring(0, 16));
+              
+              // Check if NIP-44 is available
+              const nip44Any = nostrTools.nip44 as any;
+              if (!nip44Any) {
+                console.error('❌ nostrTools.nip44 is undefined!');
+                console.log('📋 Available nostrTools modules:', Object.keys(nostrTools).join(', '));
+                throw new Error('NIP-44 module not available in nostr-tools');
+              }
+              
+              if (typeof nip44Any.decrypt !== 'function') {
+                console.error('❌ nostrTools.nip44.decrypt is not a function!');
+                console.log('📋 nip44 object properties:', Object.keys(nip44Any).join(', '));
+                throw new Error('NIP-44 decrypt function not available in nostr-tools');
+              }
+              
+              // Perform the decryption using only nostr-tools (no window.nostr!)
+              console.log('⏳ Calling nip44.decrypt with the correct parameters...');
+              console.log('📄 Ciphertext:', encryptedKey);
+              
+              console.log('🔑 Private key (hex):', privateKeyHex.substring(0, 16) + '...');
+              console.log('👤 Sender pubkey:', event.pubkey);
+              
+              // Calculate the shared secret (conversation key) between server private key and sender pubkey
+              // This is needed for NIP-44 decryption
+              const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
+              const conversationKey = nostrTools.nip44.getConversationKey(privateKeyBytes, event.pubkey);
+              
+              // Use the correct parameters for nip44.decrypt (payload, conversationKey)
+              decryptedKey = nip44Any.decrypt(encryptedKey, conversationKey);
+              console.log('✅ NIP-44 key decryption succeeded!');
+              console.log('🔑 Decrypted key:', decryptedKey.substring(0, 10) + '...');
+              
+              if (decryptionStatus) {
+                decryptionStatus.textContent = `Successfully decrypted key with server npub: ${serverNpub.substring(0, 8)}...`;
+                decryptionStatus.classList.add('success');
+              }
+            } catch (nip44Error) {
+              console.error('NIP-44 decryption error:', nip44Error);
+              if (decryptionStatus) {
+                decryptionStatus.textContent = `Decryption failed with server npub: ${serverNpub.substring(0, 8)}...`;
+                decryptionStatus.classList.add('error');
+              }
+              
+              // Fallback to using the encrypted key directly
+              console.warn('Falling back to using encrypted key directly');
+              decryptedKey = encryptedKey;
+            }
+            
+            // Now use the decrypted key to decrypt the content using Web Crypto API
+            try {
+              console.log('🔒 Attempting to decrypt content with Web Crypto API');
+              console.log('📄 Encrypted content (first 50 chars):', event.content.substring(0, 50) + '...');
+              console.log('🔑 Using decrypted key:', decryptedKey.substring(0, 10) + '...');
+              
+              // Check if the content is a valid Base64 string before attempting decryption
+              // This is important for Web Crypto API which expects properly formatted input
+              let contentToDecrypt = event.content;
+              
+              // Try to detect if the content is already in Base64 format
+              // If not, we need to encode it properly
+              try {
+                // Check if it's a valid base64 string by trying to decode it
+                atob(contentToDecrypt);
+                console.log('Content appears to be in Base64 format already');
+              // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
+              } catch (_) {
+                // If decoding fails, content is not in Base64 format
+                // Encode it properly for the crypto API
+                console.log('Content is not in Base64 format, converting...');
+                contentToDecrypt = btoa(contentToDecrypt);
+              }
+              
+              console.time('contentDecryption');
+              const decryptedEventContent = await this.httpService.decryptWithWebCrypto(
+                contentToDecrypt,
+                decryptedKey
+              );
+              console.timeEnd('contentDecryption');
+              
+              console.log('✅ Content decryption succeeded!');
+              console.log('📋 Decrypted content (first 100 chars):',
+                decryptedEventContent.substring(0, 100).replace(/\n/g, '\\n') + '...');
+              
+              decryptedContent = decryptedEventContent;
+              
+              if (decryptionStatus) {
+                decryptionStatus.textContent = `Decryption complete using server npub: ${serverNpub.substring(0, 8)}...`;
+                decryptionStatus.classList.add('success');
+              }
+            } catch (contentDecryptError) {
+              console.error('❌ Content decryption failed:', contentDecryptError);
+              console.error('📦 Error object:', JSON.stringify(contentDecryptError, Object.getOwnPropertyNames(contentDecryptError)));
+              console.log('🔑 Using decrypted key:', decryptedKey.substring(0, 10) + '...');
+              console.log('📄 Content format correct?', event.content.startsWith('Aes') ? 'Yes (starts with Aes)' : 'No');
+              
+              // Try direct decryption with NIP-44 as a fallback
+              try {
+                console.log('Trying direct content decryption with NIP-44 as fallback...');
+                const nip44Any = nostrTools.nip44 as any;
+                
+                // Calculate the shared secret (conversation key) between server private key and sender pubkey
+                // This is needed for NIP-44 decryption
+                const privateKeyBytes = new Uint8Array(privateKeyHex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
+                const conversationKey = nostrTools.nip44.getConversationKey(privateKeyBytes, event.pubkey);
+                
+                // Use the correct parameters for nip44.decrypt (payload, conversationKey)
+                const directDecryptedContent = nip44Any.decrypt(event.content, conversationKey);
+                console.log('Direct NIP-44 content decryption succeeded!');
+                decryptedContent = directDecryptedContent;
+                
+                if (decryptionStatus) {
+                  decryptionStatus.textContent = `Direct decryption succeeded with NIP-44`;
+                  decryptionStatus.classList.add('success');
+                }
+              } catch (directDecryptError) {
+                console.error('❌ Direct NIP-44 content decryption also failed:', directDecryptError);
+                
+                decryptedContent = `Key decryption successful, but content decryption failed: ${contentDecryptError instanceof Error ? contentDecryptError.message : String(contentDecryptError)}
+              
+Encrypted content: ${event.content.substring(0, 50)}...
+Decrypted key: ${decryptedKey}`;
+
+                if (decryptionStatus) {
+                  decryptionStatus.textContent = 'Content decryption failed (key decryption was successful)';
+                  decryptionStatus.classList.add('error');
+                }
+              }
+            }
+          } catch (error) {
+            console.error('Error during decryption process:', error);
+            decryptedKey = encryptedKey; // Fallback
+            decryptedContent = `[Decryption error: ${error instanceof Error ? error.message : String(error)}]\n${event.content}`;
+            
+            if (decryptionStatus) {
+              decryptionStatus.textContent = `Decryption error: ${error instanceof Error ? error.message : String(error)}`;
+              decryptionStatus.classList.add('error');
+            }
+          }
+        } catch (outerError) {
+          console.error("Outer decryption error:", outerError);
+          decryptedContent = `[NIP-44 decryption failed: ${outerError instanceof Error ? outerError.message : String(outerError)}]\n${event.content}`;
+        }
+      }
+    
+      /**
+       * Update the relay status display
+       * @param message Status message
+       * @param className CSS class for styling (connected, error, etc.)
+       */
+      public updateRelayStatus(message: string, className: string): void {
+        if (this.relayStatus) {
+          this.relayStatus.textContent = message;
+          this.relayStatus.className = `relay-status ${className}`;
+        }
+      }
+    
+      /**
+       * Filter events in the UI based on the checkbox state
+       * @param showAllEvents Whether to show all events or only those for the server
+       */
+      public filterEventsInUI(showAllEvents: boolean): void {
+        if (!this.eventsList) {
+          return;
+        }
+        
+        // Get all event items
+        const eventItems = this.eventsList.querySelectorAll('.event-item');
+        
+        // Iterate through each event item
+        eventItems.forEach((item) => {
+          const toServer = item.getAttribute('data-to-server');
+          
+          if (showAllEvents) {
+            // Show all events
+            (item as HTMLElement).style.display = '';
+          } else {
+            // Only show events addressed to our server
+            (item as HTMLElement).style.display = toServer === 'true' ? '' : 'none';
+          }
+        });
+      }
+      
+      /**
+       * Create and publish a 21121 response event for a 21120 request event
+       * @param requestEvent The 21120 request event
+       * @param responseContent The HTTP response content
+       */
+      private async create21121ResponseEvent(requestEvent: NostrEvent, responseContent: string): Promise<void> {
+        // Get the server private key
+        const serverNsec = localStorage.getItem('serverNsec');
+        if (!serverNsec) {
+          alert('Server private key (nsec) not found. Please set up a server identity first.');
+          return;
+        }
+        
+        try {
+          // Get the active relay URL
+          const relayUrl = this.nostrService.getRelayService().getActiveRelayUrl();
+          if (!relayUrl) {
+            alert('No active relay connection. Please connect to a relay first.');
+            return;
+          }
+          
+          // Create and publish the 21121 event
+          const responseEvent = await this.nostrService.createAndPublish21121Event(
+            requestEvent,
+            responseContent,
+            serverNsec,
+            relayUrl
+          );
+          
+          if (responseEvent) {
+            // Add the response to related events mapping
+            if (requestEvent.id && responseEvent.id) {
+              this.addRelatedEvent(requestEvent.id, responseEvent.id);
+            }
+            
+            // Create a toast notification
+            this.showToastNotification('21121 response event published successfully!', 'success');
+            
+            // Process the response event so it shows up in the UI
+            this.processEvent(responseEvent);
+          } else {
+            this.showToastNotification('Failed to publish 21121 response event', 'error');
+          }
+        } catch (error) {
+          console.error('Error creating 21121 response event:', error);
+          this.showToastNotification(`Error: ${error instanceof Error ? error.message : String(error)}`, 'error');
+        }
+      }
+      
+      /**
+       * Add a related event to the map
+       * @param sourceEventId The source event ID
+       * @param relatedEventId The related event ID
+       */
+      private addRelatedEvent(sourceEventId: string, relatedEventId: string): void {
+        if (!sourceEventId || !relatedEventId) return;
+        
+        // Get existing related events or create a new array
+        const related = this.relatedEvents.get(sourceEventId) || [];
+        
+        // Add the new related event if it's not already in the list
+        if (!related.includes(relatedEventId)) {
+          related.push(relatedEventId);
+          this.relatedEvents.set(sourceEventId, related);
+        }
+      }
+      
+      /**
+       * Check for 21121 responses to a 21120 request
+       * @param requestEventId The 21120 request event ID
+       */
+      private async checkFor21121Responses(requestEventId: string): Promise<void> {
+        if (!requestEventId) return;
+        
+        try {
+          // Get the active relay URL
+          const relayUrl = this.nostrService.getRelayService().getActiveRelayUrl();
+          if (!relayUrl) return;
+          
+          // Look for 21121 responses
+          const responseEvent = await this.nostrService.findResponseForRequest(requestEventId, relayUrl);
+          
+          if (responseEvent && responseEvent.id) {
+            // Add to related events
+            this.addRelatedEvent(requestEventId, responseEvent.id);
+            
+            // Process the event so it shows in the UI
+            this.processEvent(responseEvent);
+            
+            // Find the event in the UI and add a visual indicator
+            const eventItem = document.querySelector(`.event-item[data-id="${requestEventId}"]`);
+            if (eventItem) {
+              const responseIndicator = document.createElement('div');
+              responseIndicator.className = 'response-indicator';
+              responseIndicator.innerHTML = '<span class="response-available">21121 Response Available</span>';
+              eventItem.appendChild(responseIndicator);
+            }
+          }
+        } catch (error) {
+          console.error(`Error checking for 21121 responses to ${requestEventId}:`, error);
+        }
+      }
+      
+      /**
+       * Show a toast notification
+       * @param message The message to show
+       * @param type The type of notification (success, error, info)
+       */
+      private showToastNotification(message: string, type: 'success' | 'error' | 'info'): void {
+        // Create notification element
+        const notification = document.createElement('div');
+        notification.className = `toast-notification ${type}`;
+        notification.innerHTML = `
+          <div class="toast-content">
+            <span class="toast-message">${message}</span>
+            <button class="toast-close">&times;</button>
+          </div>
+        `;
+        
+        // Add to the DOM
+        document.body.appendChild(notification);
+        
+        // Add animation classes
+        setTimeout(() => {
+          notification.classList.add('show');
+        }, 10);
+        
+        // Add close button handler
+        const closeBtn = notification.querySelector('.toast-close');
+        if (closeBtn) {
+          closeBtn.addEventListener('click', () => {
+            notification.classList.remove('show');
+            setTimeout(() => {
+              document.body.removeChild(notification);
+            }, 300);
+          });
+        }
+        
+        // Auto-dismiss after 5 seconds
+        setTimeout(() => {
+          notification.classList.remove('show');
+          setTimeout(() => {
+            if (document.body.contains(notification)) {
+              document.body.removeChild(notification);
+            }
+          }, 300);
+        }, 5000);
+      }
+      
+      // Update the event
+      receivedEvent.decrypted = true;
+      receivedEvent.decryptedContent = decryptedContent;
+      this.receivedEvents.set(eventId, receivedEvent);
+      
+      // Update UI if this event is currently being viewed
+      // Since there might be issues extracting the ID from the header text with regex,
+      // let's use a more direct approach to force the UI refresh
+      if (this.eventDetails) {
+        // Always refresh the UI to show the decrypted content
+        this.showEventDetails(eventId);
+        
+        // Explicitly update the http-content element to show the decrypted content
+        const httpContentElement = this.eventDetails.querySelector('#http-content .http-content');
+        if (httpContentElement) {
+          httpContentElement.textContent = decryptedContent;
+        }
+      }
+    } catch (error) {
+      console.error("Failed to process event:", error);
+    }
+  }
+
+  /**
+   * Format HTTP content for better readability
+   * @param content The HTTP content to format
+   * @param isRequest Whether this is a request or not
+   * @param isResponse Whether this is a response or not
+   * @returns Formatted HTML
+   */
+  private formatHttpContent(content: string, isRequest: boolean, isResponse: boolean): string {
+    if (!content) {
+      return '<div class="no-content">No content to display</div>';
+    }
+
+    try {
+      const lines = content.split('\n');
+      if (lines.length === 0) {
+        return '<div class="no-content">No content to display</div>';
+      }
+
+      let formattedHtml = '';
+      
+      // Format the first line (request/status line)
+      const firstLine = lines[0];
+      formattedHtml += `<div class="${isRequest ? 'http-request-line' : 'http-status-line'}">${firstLine}</div>`;
+      
+      // Headers section
+      formattedHtml += '<div class="http-headers">';
+      
+      let inHeaders = true;
+      let headerCount = 0;
+      
+      for (let i = 1; i < lines.length; i++) {
+        const line = lines[i];
+        
+        // Empty line signifies end of headers
+        if (inHeaders && line.trim() === '') {
+          inHeaders = false;
+          formattedHtml += '</div>'; // Close headers div
+          formattedHtml += '<div class="http-body-label">Body:</div>';
+          formattedHtml += '<div class="http-body">';
+          continue;
+        }
+        
+        if (inHeaders) {
+          headerCount++;
+          // Format header with key-value separation
+          const colonIndex = line.indexOf(':');
+          if (colonIndex > 0) {
+            const key = line.substring(0, colonIndex).trim();
+            const value = line.substring(colonIndex + 1).trim();
+            formattedHtml += `<div class="http-header">
+              <span class="http-header-name">${key}:</span>
+              <span class="http-header-value">${value}</span>
+            </div>`;
+          } else {
+            // Handle malformed headers
+            formattedHtml += `<div class="http-header">${line}</div>`;
+          }
+        } else {
+          // Body content - try to detect JSON
+          if (i === lines.length - 1 && !line.trim()) {
+            // Skip empty last line
+            continue;
+          }
+          
+          formattedHtml += `<div class="http-body-line">${line}</div>`;
+        }
+      }
+      
+      // Close body div if needed
+      if (!inHeaders) {
+        formattedHtml += '</div>';
+      } else {
+        // Close headers if there was no body
+        formattedHtml += '</div>';
+        formattedHtml += '<div class="http-body-label">No body content</div>';
+      }
+      
+      return formattedHtml;
+      
+    } catch (error) {
+      console.error('Error formatting HTTP content:', error);
+      return `<pre class="http-content">${content}</pre>`;
+    }
+  }
+
+  /**
+   * Update relay status in UI
+   * @param message The status message
+   * @param className The CSS class name
+   */
+  public updateRelayStatus(message: string, className: string): void {
+    if (this.relayStatus) {
+      this.relayStatus.textContent = message;
+      this.relayStatus.className = `relay-status ${className}`;
+    }
+  }
+
+  /**
+   * Filter events in the UI based on the showAllEvents checkbox state
+   * @param showAllEvents Whether to show all events or only those for the server
+   */
+  public filterEventsInUI(showAllEvents: boolean): void {
+    if (!this.eventsList) {
+      return;
+    }
+
+    // Get all event items
+    const eventItems = this.eventsList.querySelectorAll('.event-item');
+    
+    // Iterate through each event item
+    eventItems.forEach((item) => {
+      const isToServer = item.getAttribute('data-to-server') === 'true';
+      
+      if (showAllEvents) {
+        // Show all events
+        (item as HTMLElement).style.display = '';
+      } else {
+        // Only show events addressed to the server
+        (item as HTMLElement).style.display = isToServer ? '' : 'none';
+      }
+    });
+  }
+}
diff --git a/client/src/services/WebSocketManager.ts b/client/src/services/WebSocketManager.ts
index 5c139b5..5d67502 100644
--- a/client/src/services/WebSocketManager.ts
+++ b/client/src/services/WebSocketManager.ts
@@ -5,12 +5,9 @@
 
 export interface WebSocketOptions {
   timeout?: number;
-  // eslint-disable-next-line no-unused-vars
-  onOpen?: (socket: WebSocket) => void;
-  // eslint-disable-next-line no-unused-vars
-  onMessage?: (data: unknown) => void;
-  // eslint-disable-next-line no-unused-vars
-  onError?: (error: Event) => void;
+  onOpen?: (ws: WebSocket) => void;
+  onMessage?: (parsedData: unknown) => void;
+  onError?: (evt: Event) => void;
   onClose?: () => void;
 }
 
@@ -21,20 +18,15 @@ export class WebSocketManager {
 
   /**
    * Create a WebSocket connection
-   * @param url The WebSocket URL to connect to
-   * @param options Options for the WebSocket connection
-   * @returns A promise that resolves when the connection is established
    */
   public async connect(url: string, options: WebSocketOptions = {}): Promise<WebSocket> {
     return new Promise<WebSocket>((resolve, reject) => {
-      // Close existing connection if any
       this.close();
       
       this.url = url;
       this.ws = new WebSocket(url);
       this.connected = false;
       
-      // Set a timeout to avoid hanging
       const timeout = setTimeout(() => {
         if (!this.connected) {
           this.close();
@@ -42,16 +34,12 @@ export class WebSocketManager {
         }
       }, options.timeout || 5000);
       
-      // Set up event handlers
       this.ws.onopen = () => {
         clearTimeout(timeout);
         this.connected = true;
-        
         if (options.onOpen) {
-          const callback = options.onOpen;
-          callback(this.ws as WebSocket);
+          options.onOpen(this.ws as WebSocket);
         }
-        
         resolve(this.ws as WebSocket);
       };
       
@@ -59,8 +47,7 @@ export class WebSocketManager {
         if (options.onMessage && typeof msg.data === 'string') {
           try {
             const parsedData = JSON.parse(msg.data);
-            const callback = options.onMessage;
-            callback(parsedData);
+            options.onMessage(parsedData);
           } catch {
             // Ignore parsing errors
           }
@@ -69,12 +56,9 @@ export class WebSocketManager {
       
       this.ws.onerror = (errorEvt) => {
         clearTimeout(timeout);
-        
         if (options.onError) {
-          const callback = options.onError;
-          callback(errorEvt);
+          options.onError(errorEvt);
         }
-        
         if (!this.connected) {
           reject(new Error(`WebSocket error: ${errorEvt.toString()}`));
         }
@@ -83,7 +67,6 @@ export class WebSocketManager {
       this.ws.onclose = () => {
         clearTimeout(timeout);
         this.connected = false;
-        
         if (options.onClose) {
           options.onClose();
         }
@@ -93,9 +76,6 @@ export class WebSocketManager {
   
   /**
    * Test a WebSocket connection without creating a persistent connection
-   * @param url The WebSocket URL to test
-   * @param timeout Timeout in milliseconds
-   * @returns A promise that resolves when the connection test is complete
    */
   public async testConnection(url: string, timeout = 5000): Promise<boolean> {
     try {
@@ -131,8 +111,6 @@ export class WebSocketManager {
   
   /**
    * Send data through the WebSocket
-   * @param data The data to send
-   * @returns true if sent successfully, false otherwise
    */
   public send(data: string | ArrayBufferLike | Blob | ArrayBufferView): boolean {
     if (!this.ws || !this.connected) {
diff --git a/client/src/theme-utils.ts b/client/src/theme-utils.ts
new file mode 100644
index 0000000..106c693
--- /dev/null
+++ b/client/src/theme-utils.ts
@@ -0,0 +1,78 @@
+/**
+ * theme-utils.ts
+ * Utility functions for theme management
+ */
+
+/**
+ * Initialize the theme based on localStorage settings
+ * This should be called on page load
+ */
+export function initializeTheme(): void {
+  console.log('Initializing theme from localStorage');
+  
+  const savedTheme = localStorage.getItem('theme');
+  if (savedTheme === 'dark') {
+    document.body.setAttribute('data-theme', 'dark');
+    console.log('Set dark theme from localStorage');
+    // Remove any inline background-color style that might override CSS variables
+    document.body.style.removeProperty('background-color');
+  } else {
+    document.body.removeAttribute('data-theme');
+    console.log('Set light theme (default) from localStorage or no setting');
+  }
+  
+  // Update the theme icon if it exists
+  updateThemeIcon();
+}
+
+/**
+ * Toggle between light and dark themes
+ */
+export function toggleTheme(): void {
+  const body = document.body;
+  const isDarkMode = body.getAttribute('data-theme') === 'dark';
+  
+  console.log('Toggling theme, current is dark:', isDarkMode);
+  
+  if (isDarkMode) {
+    // Switch to light theme
+    body.removeAttribute('data-theme');
+    localStorage.setItem('theme', 'light');
+    console.log('Switched to light theme');
+  } else {
+    // Switch to dark theme
+    body.setAttribute('data-theme', 'dark');
+    localStorage.setItem('theme', 'dark');
+    console.log('Switched to dark theme');
+  }
+  
+  // Remove any inline background-color style that might override CSS variables
+  body.style.removeProperty('background-color');
+  
+  // Update the theme icon
+  updateThemeIcon();
+}
+
+/**
+ * Update the theme icon based on current theme
+ */
+export function updateThemeIcon(): void {
+  const isDarkMode = document.body.getAttribute('data-theme') === 'dark';
+  const themeIcon = document.getElementById('themeIcon');
+  
+  if (themeIcon) {
+    themeIcon.textContent = isDarkMode ? '☀️' : '🌙';
+    console.log('Updated theme icon to:', themeIcon.textContent);
+  } else {
+    console.log('Theme icon element not found');
+  }
+}
+
+// Initialize the theme automatically when this file is imported
+document.addEventListener('DOMContentLoaded', initializeTheme);
+
+// Add a fallback initialization for cases where DOMContentLoaded already fired
+if (document.readyState === 'complete' || document.readyState === 'interactive') {
+  console.log('Document already loaded, initializing theme immediately');
+  initializeTheme();
+}
\ No newline at end of file
diff --git a/client/src/types/window.d.ts b/client/src/types/window.d.ts
new file mode 100644
index 0000000..01ee3ed
--- /dev/null
+++ b/client/src/types/window.d.ts
@@ -0,0 +1,38 @@
+// Type definitions for window.nostr (NIP-07 browser extension)
+import type { NostrEvent } from '../relay';
+
+// Define the extension interfaces here to avoid conflicts
+interface WindowNostrExtension {
+  /**
+   * Get the user's public key from the Nostr extension
+   * @returns The user's public key as a hex string
+   */
+  getPublicKey: () => Promise<string>;
+  
+  /**
+   * Sign an event using the Nostr extension
+   * @param event The unsigned event to sign
+   * @returns The signed event
+   */
+  signEvent: (event: Partial<NostrEvent>) => Promise<NostrEvent>;
+  
+  /**
+   * NIP-44 encryption methods (may not be available in all extensions)
+   */
+  nip44?: {
+    encrypt(plaintext: string, pubkey: string): Promise<string>;
+    decrypt(ciphertext: string, pubkey: string): Promise<string>;
+  };
+}
+
+// We don't want to modify the original declaration in converter.ts,
+// so we'll augment it with our own declaration here
+declare global {
+  // Augment the Window interface but don't redefine the nostr property
+  interface Window {
+    // This is intentionally left empty
+  }
+}
+
+// This file is a module declaration
+export {};
\ No newline at end of file
diff --git a/client/src/utils.ts b/client/src/utils.ts
index 09be12e..388f52b 100644
--- a/client/src/utils.ts
+++ b/client/src/utils.ts
@@ -132,4 +132,8 @@ export function showSuccess(element: HTMLElement, message: string): void {
 export function showLoading(element: HTMLElement, message: string = 'Processing...'): void {
     element.innerHTML = `<span>${message}</span>`;
     element.style.display = 'block';
-}
\ No newline at end of file
+}
+/**
+ * Theme functionality has been moved to theme-utils.ts
+ * Import { toggleTheme } from './theme-utils' instead
+ */
\ No newline at end of file
diff --git a/client/src/utils/crypto-utils.ts b/client/src/utils/crypto-utils.ts
new file mode 100644
index 0000000..1d5a462
--- /dev/null
+++ b/client/src/utils/crypto-utils.ts
@@ -0,0 +1,319 @@
+/**
+ * Crypto utilities for WebCrypto operations, NIP-44 encryption, and key utilities
+ * Extracted for better modularity
+ */
+
+// Import specific utilities from nostr-tools
+import { nip19 } from 'nostr-tools';
+
+/**
+ * Validates an npub and returns the hex pubkey
+ * @param npub The npub to validate and convert
+ * @returns The hex pubkey
+ * @throws Error if npub is invalid
+ */
+export function npubToHex(npub: string): string {
+  // Require npub format to ensure checksum validation
+  if (!npub.startsWith('npub')) {
+    throw new Error('Pubkey must be in npub format for validation');
+  }
+  
+  try {
+    const { type, data } = nip19.decode(npub);
+    
+    if (type !== 'npub') {
+      throw new Error(`Not an npub: decoded type is ${type}`);
+    }
+    
+    // Return the hex pubkey without length assumptions
+    return data as string;
+  } catch (error) {
+    throw new Error(`Invalid npub: ${error instanceof Error ? error.message : String(error)}`);
+  }
+}
+
+/**
+ * Encrypt data using Web Crypto API with AES-GCM
+ * @param plaintext Text content to encrypt
+ * @param key Encryption key (will be hashed with SHA-256)
+ * @returns Promise resolving to base64-encoded encrypted data
+ */
+export async function encryptWithWebCrypto(plaintext: string, key: string): Promise<string> {
+  try {
+    // Generate a random 12-byte IV
+    const iv = crypto.getRandomValues(new Uint8Array(12));
+    
+    // Create key material from the encryption key
+    const keyMaterial = await crypto.subtle.digest(
+      'SHA-256',
+      new TextEncoder().encode(key)
+    );
+    
+    // Import the key for AES-GCM encryption
+    const cryptoKey = await crypto.subtle.importKey(
+      'raw',
+      keyMaterial,
+      { name: 'AES-GCM' },
+      false,
+      ['encrypt']
+    );
+    
+    // Encrypt the data
+    const encryptedData = await crypto.subtle.encrypt(
+      { name: 'AES-GCM', iv },
+      cryptoKey,
+      new TextEncoder().encode(plaintext)
+    );
+    
+    // Combine IV and ciphertext
+    const encryptedBytes = new Uint8Array(iv.length + new Uint8Array(encryptedData).length);
+    encryptedBytes.set(iv, 0);
+    encryptedBytes.set(new Uint8Array(encryptedData), iv.length);
+    
+    // Return base64 encoded encrypted data
+    return btoa(String.fromCharCode(...encryptedBytes));
+  } catch (error) {
+    throw new Error(`WebCrypto encryption failed: ${error instanceof Error ? error.message : String(error)}`);
+  }
+}
+
+/**
+ * Decrypt data using Web Crypto API with AES-GCM
+ * @param encryptedBase64 Base64-encoded encrypted data
+ * @param key Decryption key (will be hashed with SHA-256)
+ * @returns Promise resolving to decrypted plaintext
+ */
+export async function decryptWithWebCrypto(encryptedBase64: string, key: string): Promise<string> {
+  try {
+    // Convert base64 to byte array
+    const encryptedBytes = new Uint8Array(
+      atob(encryptedBase64)
+        .split('')
+        .map(char => char.charCodeAt(0))
+    );
+    
+    // Extract IV (first 12 bytes)
+    const iv = encryptedBytes.slice(0, 12);
+    // Extract ciphertext (remaining bytes)
+    const ciphertext = encryptedBytes.slice(12);
+    
+    // Create key material from the decryption key
+    const keyMaterial = await crypto.subtle.digest(
+      'SHA-256',
+      new TextEncoder().encode(key)
+    );
+    
+    // Import the key for AES-GCM decryption
+    const cryptoKey = await crypto.subtle.importKey(
+      'raw',
+      keyMaterial,
+      { name: 'AES-GCM' },
+      false,
+      ['decrypt']
+    );
+    
+    // Decrypt the data
+    const decryptedData = await crypto.subtle.decrypt(
+      { name: 'AES-GCM', iv },
+      cryptoKey,
+      ciphertext
+    );
+    
+    // Convert decrypted data to string
+    return new TextDecoder().decode(decryptedData);
+  } catch (error) {
+    throw new Error(`WebCrypto decryption failed: ${error instanceof Error ? error.message : String(error)}`);
+  }
+}
+
+/**
+ * Encrypt a key using NIP-44 via window.nostr extension
+ * This is specifically for encrypting keys, not large content
+ *
+ * @param plaintext The encryption key to encrypt
+ * @param pubkey Target public key in hex format
+ * @returns Promise resolving to NIP-44 encrypted key
+ */
+export async function encryptKeyWithNostrExtension(plaintext: string, pubkey: string): Promise<string> {
+  try {
+    // Convert to hex if it's an npub
+    const hexPubkey = pubkey.startsWith('npub') ? npubToHex(pubkey) : pubkey;
+
+    // Check if window.nostr.nip44 is available
+    if (!window.nostr || typeof window.nostr.nip44 !== 'object' || typeof window.nostr.nip44.encrypt !== 'function') {
+      throw new Error("NIP-44 encryption not available - connect to a compatible NIP-07 extension");
+    }
+
+    // Encrypt using NIP-44 through the extension
+    // The browser extension is expected to handle key derivation and encryption
+    const encryptedKey = await window.nostr.nip44.encrypt(plaintext, hexPubkey);
+    return encryptedKey;
+  } catch (error) {
+    throw new Error(`NIP-44 encryption failed: ${error instanceof Error ? error.message : String(error)}`);
+  }
+}
+
+/**
+ * Decrypt a key using NIP-44 via window.nostr extension
+ * This is specifically for decrypting keys, not large content
+ *
+ * @param ciphertext The encrypted key
+ * @param pubkey Source public key (npub or hex)
+ * @returns Promise resolving to decrypted key
+ */
+export async function decryptKeyWithNostrExtension(ciphertext: string, pubkey: string): Promise<string> {
+  try {
+    // Convert to hex if it's an npub
+    const hexPubkey = pubkey.startsWith('npub') ? npubToHex(pubkey) : pubkey;
+    
+    // Validate that it's a proper pubkey format (64 hex chars)
+    if (!/^[0-9a-f]{64}$/i.test(hexPubkey)) {
+      console.error("Invalid pubkey format for NIP-44 decryption:", {
+        originalPubkey: pubkey.substring(0, 10) + '...',
+        convertedHexPubkey: hexPubkey.substring(0, 10) + '...',
+        length: hexPubkey.length
+      });
+      throw new Error(`Invalid pubkey format: ${hexPubkey.substring(0, 10)}... (length: ${hexPubkey.length})`);
+    }
+
+    // Check if window.nostr.nip44 is available
+    if (!window.nostr || typeof window.nostr.nip44 !== 'object' || typeof window.nostr.nip44.decrypt !== 'function') {
+      throw new Error("NIP-44 decryption not available - connect to a compatible NIP-07 extension");
+    }
+
+    console.log("Using pubkey for NIP-44 decryption with Nostr extension:", {
+      hexPubkey: hexPubkey.substring(0, 10) + '...',
+      length: hexPubkey.length,
+      ciphertextLength: ciphertext.length
+    });
+
+    // Decrypt using NIP-44 through the extension
+    // The browser extension is expected to handle key derivation and decryption
+    const decryptedKey = await window.nostr.nip44.decrypt(ciphertext, hexPubkey);
+    return decryptedKey;
+  } catch (error) {
+    throw new Error(`NIP-44 decryption failed: ${error instanceof Error ? error.message : String(error)}`);
+  }
+}
+
+/**
+ * Decrypt a key using NIP-44 with nostr-tools library directly
+ * This is specifically for server-side decryption using the server's private key
+ *
+ * @param ciphertext The encrypted key
+ * @param serverNsec Server's private key in nsec format
+ * @param clientPubkey Optional client's public key for conversation key derivation
+ * @returns Promise resolving to decrypted key
+ */
+export function decryptKeyWithNostrTools(ciphertext: string, serverNsec: string, clientPubkey?: string): Promise<string> {
+  return new Promise<string>(async (resolve, reject) => {
+    try {
+      if (!serverNsec.startsWith('nsec')) {
+        reject(new Error("Server key must be in nsec format for decryption"));
+        return;
+      }
+  
+      // Import nostr-tools library
+      const nostrTools = await import('nostr-tools');
+      
+      // Check if NIP-44 implementation is available in nostr-tools
+      if (!nostrTools.nip44) {
+        reject(new Error("NIP-44 not available in nostr-tools - this may need to be implemented"));
+        return;
+      }
+  
+      // Decode the nsec to get the raw private key
+      const decoded = nostrTools.nip19.decode(serverNsec);
+      if (decoded.type !== 'nsec') {
+        reject(new Error(`Not a private key: decoded type is ${decoded.type}`));
+        return;
+      }
+      
+      // Handle the different types correctly
+      const privateKeyBytes = decoded.data as Uint8Array;
+      const privateKeyHex = Buffer.from(privateKeyBytes).toString('hex');
+      
+      // Get the server's public key
+      // Use privateKeyBytes directly which is what nostrTools.getPublicKey expects
+      const serverPubkey = nostrTools.getPublicKey(privateKeyBytes);
+      
+      // Check if we have the client's public key for proper conversation key derivation
+      if (clientPubkey) {
+        console.log("Using client pubkey for conversation key derivation:", clientPubkey.substring(0, 8) + "...");
+        
+        try {
+          // Convert to hex if it's an npub
+          const clientPubkeyHex = clientPubkey.startsWith('npub')
+            ? (nostrTools.nip19.decode(clientPubkey).data as string)
+            : clientPubkey;
+            
+          // Follow the NIP-44 example with conversation keys
+          console.log("Deriving conversation key between server and client");
+          
+          // getConversationKey actually takes bytes for the private key
+          // The second parameter can be a string for the pubkey
+          const conversationKey = await nostrTools.nip44.getConversationKey(
+            privateKeyBytes,  // Server private key as bytes
+            clientPubkeyHex   // Client public key as hex string
+          );
+          
+          console.log("Generated conversation key for decryption");
+          
+          // decrypt signature: function decrypt(payload, conversationKey)
+          // Only need payload and conversationKey
+          const decryptedMessage = await nostrTools.nip44.decrypt(
+            ciphertext,      // The encrypted payload
+            conversationKey  // The conversation key we derived
+          );
+          
+          console.log("Successfully decrypted key with NIP-44 conversation key");
+          resolve(decryptedMessage);
+          return;
+        } catch (conversationError) {
+          console.error("Conversation key approach failed:", conversationError);
+          // Fall through to direct decryption as fallback
+        }
+      }
+  
+      // If client pubkey not available or conversation key approach failed,
+      // try special case handling for short keys that might not be encrypted
+      if (ciphertext.length <= 32 && ciphertext.length > 0) {
+        console.log("Short ciphertext detected, might be an unencrypted key:", ciphertext);
+        resolve(ciphertext);
+        return;
+      }
+      
+      // Otherwise, we need to create a self-conversation key with the server's key for both sides
+      console.log("Falling back to self-conversation key for decryption");
+      try {
+        // Get a conversation key where server talks to itself
+        const selfConversationKey = await nostrTools.nip44.getConversationKey(
+          privateKeyBytes,  // Server private key as bytes
+          serverPubkey     // Server's own public key
+        );
+        
+        // Try decrypting with this self-conversation key
+        const decryptedKey = await nostrTools.nip44.decrypt(
+          ciphertext,
+          selfConversationKey
+        );
+        
+        console.log("Successfully decrypted key with self-conversation key");
+        resolve(decryptedKey);
+      } catch (decryptError) {
+        console.error("Self-conversation key decryption failed:", decryptError);
+        
+        // Last resort: if we have a short string, it might be unencrypted
+        if (ciphertext.length <= 32 && ciphertext.length > 0) {
+          console.log("Short ciphertext detected, using as-is");
+          resolve(ciphertext);
+        } else {
+          reject(new Error(`NIP-44 decryption failed: ${decryptError instanceof Error ? decryptError.message : String(decryptError)}`));
+        }
+      }
+    } catch (error) {
+      console.error("Error in decryptKeyWithNostrTools:", error);
+      reject(new Error(`NIP-44 decryption with nostr-tools failed: ${error instanceof Error ? error.message : String(error)}`));
+    }
+  });
+}
\ No newline at end of file
diff --git a/client/styles.css b/client/styles.css
index f414e06..5a1493b 100644
--- a/client/styles.css
+++ b/client/styles.css
@@ -1,3 +1,103 @@
+/* Radio button styling for pubkey options */
+.pubkey-options {
+    margin: 12px 0;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+}
+
+.radio-option {
+    margin-bottom: 8px;
+    display: flex;
+    align-items: center;
+    color: var(--text-primary);
+    padding: 8px 12px;
+    background-color: var(--bg-secondary);
+    border-radius: 6px;
+    transition: background-color 0.2s ease;
+}
+
+.radio-option:hover {
+    background-color: var(--hover-color);
+}
+
+.radio-option input[type="radio"] {
+    margin-right: 10px;
+    cursor: pointer;
+    width: 16px;
+    height: 16px;
+}
+
+/* Two column layout for the billboard modal */
+.modal-columns {
+    display: flex;
+    gap: 15px;
+    margin-bottom: 10px;
+}
+
+.modal-column {
+    flex: 1;
+    min-width: 0;
+}
+
+@media (max-width: 768px) {
+    .modal-columns {
+        flex-direction: column;
+        gap: 5px;
+    }
+}
+
+.radio-option label {
+    cursor: pointer;
+    font-weight: normal;
+    margin-bottom: 0;
+}
+
+.form-hint {
+    font-size: 0.85em;
+    color: var(--text-tertiary, #666);
+    margin: 0 0 10px 0;
+    padding: 8px;
+    background-color: var(--bg-info);
+    border-left: 3px solid var(--accent-color);
+    border-radius: 0 4px 4px 0;
+    line-height: 1.4;
+}
+
+.field-help {
+    font-size: 0.8em;
+    color: var(--text-secondary);
+    margin-top: 4px;
+    font-style: italic;
+}
+
+.hidden {
+    display: none;
+}
+
+/* Server key format toggle styling */
+.server-format-button {
+    background-color: var(--button-secondary, #6c757d);
+    color: var(--text-color-on-primary, white);
+    border: none;
+    padding: 8px 12px;
+    border-radius: 4px;
+    cursor: pointer;
+    font-size: 0.8rem;
+    margin-right: 5px;
+}
+
+.server-format-button:hover {
+    background-color: var(--button-secondary-hover, #5a6268);
+}
+
+.format-indicator {
+    font-size: 0.8rem;
+    color: var(--text-secondary, #6c757d);
+    margin-top: 4px;
+    margin-bottom: 10px;
+}
+
 /* Modal styles */
 .modal {
     display: none;
@@ -12,49 +112,84 @@
 
 .modal-content {
     background-color: var(--background-color);
-    margin: 10% auto;
-    padding: 20px;
+    margin: 5% auto;
+    padding: 16px;
     border: 1px solid var(--border-color);
     width: 80%;
     max-width: 600px;
-    border-radius: 8px;
-    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+    border-radius: 12px;
+    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
+    max-height: 90vh;
+    overflow-y: auto;
 }
 
 .modal-header {
     display: flex;
     justify-content: space-between;
     align-items: center;
-    margin-bottom: 15px;
-    padding-bottom: 10px;
-    border-bottom: 1px solid var(--border-color);
+    margin-bottom: 20px;
+    padding-bottom: 12px;
+    border-bottom: 2px solid var(--accent-color);
 }
 
 .modal-header h3 {
     margin: 0;
-    color: var(--text-color);
+    color: var(--text-primary);
+    font-size: 1.4em;
+    font-weight: 600;
 }
 
 .close-modal {
     color: var(--text-secondary);
-    font-size: 24px;
+    font-size: 28px;
     font-weight: bold;
     cursor: pointer;
+    height: 36px;
+    width: 36px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 50%;
+    transition: all 0.2s ease;
 }
 
 .close-modal:hover {
-    color: var(--text-color);
+    color: var(--text-primary);
+    background-color: var(--hover-color);
+    transform: rotate(90deg);
+}
+.form-group {
+    margin-bottom: 12px;
+    padding: 10px;
+    background-color: var(--bg-tertiary);
+    border-radius: 8px;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+    transition: all 0.2s ease;
 }
 
-.form-group {
-    margin-bottom: 15px;
+.expiry-input-container {
+    display: flex;
+    align-items: center;
+    width: 100%;
+}
+
+.expiry-input-container input {
+    flex: 0 0 100px;
+    margin-right: 10px;
+}
+
+.expiry-unit {
+    font-size: 1em;
+    color: var(--text-secondary);
+    font-weight: 500;
 }
 
 .form-group label {
     display: block;
-    margin-bottom: 5px;
-    font-weight: bold;
-    color: var(--text-color);
+    margin-bottom: 4px;
+    font-weight: 600;
+    color: var(--text-primary);
+    font-size: 0.95em;
 }
 
 .form-group input,
@@ -62,43 +197,60 @@
     width: 100%;
     padding: 8px;
     border: 1px solid var(--border-color);
-    border-radius: 4px;
+    border-radius: 6px;
     background-color: var(--input-background);
-    color: var(--text-color);
+    color: var(--text-primary);
+    font-size: 0.95em;
+    transition: border-color 0.2s ease, box-shadow 0.2s ease;
 }
 
+.form-group input:focus,
+.form-group textarea:focus {
+    border-color: var(--accent-color);
+    box-shadow: 0 0 0 2px rgba(63, 135, 255, 0.2);
+    outline: none;
+}
 .form-actions {
     display: flex;
     justify-content: flex-end;
-    gap: 10px;
-    margin-top: 20px;
+    gap: 8px;
+    margin-top: 16px;
+    padding-top: 10px;
+    border-top: 1px solid var(--border-color);
 }
-
 .primary-button {
-    background-color: var(--primary-color);
+    background-color: var(--button-primary, #0d6efd);
     color: white;
     border: none;
-    padding: 8px 16px;
-    border-radius: 4px;
+    padding: 12px 20px;
+    border-radius: 6px;
     cursor: pointer;
     font-weight: bold;
+    font-size: 1.05em;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+    transition: all 0.2s ease;
 }
 
 .secondary-button {
     background-color: transparent;
-    color: var(--text-color);
+    color: var(--text-primary);
     border: 1px solid var(--border-color);
-    padding: 8px 16px;
-    border-radius: 4px;
+    padding: 12px 20px;
+    border-radius: 6px;
     cursor: pointer;
+    font-size: 1.05em;
+    transition: all 0.2s ease;
 }
 
 .primary-button:hover {
-    background-color: var(--primary-hover);
+    background-color: var(--button-hover);
+    transform: translateY(-2px);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
 }
 
 .secondary-button:hover {
     background-color: var(--hover-color);
+    transform: translateY(-2px);
 }
 
 /* Billboard specific styles */
@@ -106,6 +258,48 @@
     margin: 20px 0;
     display: flex;
     justify-content: flex-end;
+    padding: 10px;
+    background-color: var(--bg-secondary);
+    border-radius: 8px;
+    border: 1px solid var(--border-color);
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+/* Dark mode specific styling for billboard actions */
+body[data-theme="dark"] .billboard-actions {
+    background-color: var(--bg-tertiary);
+    border-color: rgba(255, 255, 255, 0.1);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+}
+
+/* Make "Add New Billboard" button more prominent */
+#createBillboardBtn {
+    background-color: var(--button-primary);
+    color: white;
+    font-weight: bold;
+    padding: 10px 16px;
+    border-radius: 4px;
+    cursor: pointer;
+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
+    border: none;
+    position: relative;
+    overflow: hidden;
+}
+
+/* Add a glowing effect in dark mode */
+body[data-theme="dark"] #createBillboardBtn {
+    border: 1px solid rgba(255, 255, 255, 0.2);
+    box-shadow: 0 0 10px rgba(63, 135, 255, 0.5);
+}
+
+#createBillboardBtn:hover {
+    background-color: var(--button-hover);
+    transform: translateY(-1px);
+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
+}
+
+body[data-theme="dark"] #createBillboardBtn:hover {
+    box-shadow: 0 0 15px rgba(63, 135, 255, 0.8);
 }
 
 .billboard-edit-btn,
@@ -132,6 +326,29 @@
     background-color: var(--hover-color);
 }
 
+/* User-owned event styling */
+.user-owned-indicator {
+    font-size: 0.8rem;
+    color: var(--button-success, #28a745);
+    margin-left: 10px;
+    font-weight: normal;
+}
+
+.billboard-card.user-owned {
+    border-left: 3px solid var(--button-success, #28a745);
+}
+
+/* Disabled edit button */
+.billboard-edit-btn:disabled {
+    color: var(--text-tertiary, #6c757d);
+    cursor: not-allowed;
+    opacity: 0.6;
+}
+
+.billboard-edit-btn:disabled:hover {
+    background-color: transparent;
+}
+
 /* Styles for the HTTP Messages Project Homepage */
 
 /* CSS Variables for themes */
@@ -152,10 +369,15 @@
     --button-success-hover: #218838;
     --info-border: #0d6efd;
     --code-bg: #f8f9fa;
+    --hover-color: rgba(0, 0, 0, 0.05);
+    --input-background: #ffffff;
+    --button-secondary: #6c757d;
+    --button-secondary-hover: #5a6268;
+    --background-color: #ffffff;
 }
 
 /* Dark theme */
-[data-theme="dark"] {
+body[data-theme="dark"] {
     --bg-primary: #121212;
     --bg-secondary: #1e1e1e;
     --bg-tertiary: #252836;
@@ -171,6 +393,11 @@
     --button-success-hover: #218838;
     --info-border: #3f87ff;
     --code-bg: #252525;
+    --hover-color: rgba(255, 255, 255, 0.1);
+    --input-background: #2a2a2a;
+    --button-secondary: #444444;
+    --button-secondary-hover: #555555;
+    --background-color: #1e1e1e;
 }
 
 /* General layout */
@@ -182,7 +409,12 @@ body {
     background-color: var(--bg-primary);
     color: var(--text-primary);
     line-height: 1.6;
-    transition: background-color 0.3s ease, color 0.3s ease;
+    transition: all 0.3s ease;
+}
+
+/* Make sure all elements using CSS vars also transition smoothly */
+* {
+    transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
 }
 
 /* Headings */
@@ -258,6 +490,26 @@ body {
     color: var(--accent-color);
 }
 
+/* Nuclear reset button in nav bar */
+.nuclear-reset-btn {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: transparent;
+    color: var(--text-primary);
+    border: none;
+    cursor: pointer;
+    font-size: 16px;
+    transition: all 0.3s ease;
+    padding: 0;
+    margin: 0 10px 0 0;
+}
+
+.nuclear-reset-btn:hover {
+    color: #e74c3c;
+    transform: scale(1.2);
+}
+
 /* Ensure icons are properly sized */
 #themeIcon {
     font-size: 18px;
@@ -576,6 +828,81 @@ footer {
     box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
 }
 
+/* Tab content visibility control */
+.tab-content {
+  display: none;
+}
+
+.tab-content.active {
+  display: block;
+}
+
+/* HTTP Response Modal */
+.http-response-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-color: rgba(0, 0, 0, 0.7);
+  z-index: 1000;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+.http-response-container {
+  background-color: var(--bg-primary);
+  border-radius: 8px;
+  width: 80%;
+  max-width: 800px;
+  max-height: 90vh;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
+}
+
+.http-response-header {
+  background-color: var(--bg-secondary);
+  padding: 15px 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  border-bottom: 1px solid var(--border-color);
+}
+
+.http-response-header h3 {
+  margin: 0;
+  font-size: 1.2rem;
+  color: var(--text-primary);
+}
+
+.http-response-tabs {
+  display: flex;
+  background-color: var(--bg-secondary);
+  border-bottom: 1px solid var(--border-color);
+  padding: 0 10px;
+}
+
+.http-response-content {
+  flex: 1;
+  overflow-y: auto;
+  padding: 15px;
+}
+
+.close-modal-btn {
+  background: none;
+  border: none;
+  font-size: 1.5rem;
+  cursor: pointer;
+  color: var(--text-secondary);
+}
+
+.close-modal-btn:hover {
+  color: var(--text-primary);
+}
+
 .selection-header {
     padding: 10px;
     border-bottom: 1px solid var(--border-color);
@@ -792,6 +1119,18 @@ footer {
     gap: 10px;
 }
 
+/* Server section styles */
+.server-section {
+    margin-bottom: 20px;
+    padding: 15px;
+    background-color: var(--bg-secondary);
+    border-radius: 8px;
+    border: 1px solid var(--border-color);
+    display: flex;
+    flex-direction: column;
+    gap: 15px;
+}
+
 .server-npub-container label {
     font-weight: 600;
     min-width: 100px;
@@ -890,21 +1229,27 @@ footer {
 }
 
 .relay-status.connected {
-    background-color: #d4edda;
-    color: #155724;
-    border: 1px solid #c3e6cb;
+    background-color: var(--bg-info);
+    color: var(--button-success);
+    border: 1px solid var(--button-success);
 }
 
 .relay-status.connecting {
-    background-color: #fff3cd;
-    color: #856404;
-    border: 1px solid #ffeeba;
+    background-color: var(--bg-info);
+    color: var(--text-secondary);
+    border: 1px solid var(--border-color);
 }
 
 .relay-status.error {
-    background-color: #f8d7da;
-    color: #721c24;
-    border: 1px solid #f5c6cb;
+    background-color: var(--bg-info);
+    color: #e74c3c;
+    border: 1px solid #e74c3c;
+}
+
+.relay-status.notice {
+    background-color: var(--bg-info);
+    color: var(--text-secondary);
+    border: 1px solid var(--border-color);
 }
 
 .subscription-settings {
@@ -1011,15 +1356,15 @@ footer {
 }
 
 .connection-status-indicator.connected {
-    background-color: #d4edda;
-    color: #155724;
-    border: 1px solid #c3e6cb;
+    background-color: var(--bg-info);
+    color: var(--button-success);
+    border: 1px solid var(--button-success);
 }
 
 .connection-status-indicator.not-connected {
-    background-color: #f8d7da;
-    color: #721c24;
-    border: 1px solid #f5c6cb;
+    background-color: var(--bg-info);
+    color: #e74c3c;
+    border: 1px solid #e74c3c;
 }
 
 .connect-button {
@@ -1489,14 +1834,42 @@ footer {
     border-left: 4px solid var(--accent-color);
     margin: 0;
 }
+/* Notice for missing server registration */
+.info-notice {
+    margin: 15px 0;
+    padding: 15px;
+    background-color: var(--bg-info);
+    border-radius: 8px;
+    border: 1px solid var(--border-color);
+    color: var(--text-secondary);
+    line-height: 1.5;
+}
+
+.info-notice p {
+    margin-bottom: 10px;
+}
+
+.info-notice p:last-child {
+    margin-bottom: 0;
+}
+
+.info-notice a {
+    color: var(--accent-color);
+    text-decoration: underline;
+    font-weight: 500;
+}
+
+.info-notice a:hover {
+    text-decoration: none;
+}
 
 .decryption-status {
     margin-top: 10px;
     padding: 8px;
     border-radius: 4px;
-    background-color: #fff3cd;
-    color: #856404;
-    border: 1px solid #ffeeba;
+    background-color: var(--bg-info);
+    color: var(--text-secondary);
+    border: 1px solid var(--border-color);
     text-align: center;
     font-style: italic;
     font-size: 14px;
@@ -1504,15 +1877,15 @@ footer {
 }
 
 .decryption-status.success {
-    background-color: #d4edda;
-    color: #155724;
-    border: 1px solid #c3e6cb;
+    background-color: var(--bg-info);
+    color: var(--button-success);
+    border: 1px solid var(--button-success);
 }
 
 .decryption-status.error {
-    background-color: #f8d7da;
-    color: #721c24;
-    border: 1px solid #f5c6cb;
+    background-color: var(--bg-info);
+    color: #e74c3c;
+    border: 1px solid #e74c3c;
 }
 
 .content-header {
@@ -1761,6 +2134,170 @@ footer {
     white-space: pre-wrap;
     word-break: break-word;
 }
+
+/* HTTP Content Formatting Styles */
+.http-formatted-container {
+  background-color: var(--bg-secondary);
+  border-radius: 6px;
+  overflow: hidden;
+  font-family: monospace;
+  border: 1px solid var(--border-color);
+  margin-bottom: 15px;
+}
+
+.http-request-line, .http-status-line {
+  padding: 10px;
+  background-color: var(--accent-color);
+  color: white;
+  font-weight: bold;
+  white-space: pre-wrap;
+  overflow-x: auto;
+}
+
+.http-headers {
+  padding: 10px;
+  border-bottom: 1px solid var(--border-color);
+  background-color: var(--bg-secondary);
+}
+
+.http-header {
+  display: flex;
+  margin-bottom: 5px;
+  border-bottom: 1px dashed var(--border-color);
+  padding-bottom: 5px;
+}
+
+.http-header:last-child {
+  border-bottom: none;
+  margin-bottom: 0;
+  padding-bottom: 0;
+}
+
+.http-header-name {
+  color: var(--accent-color);
+  font-weight: bold;
+  margin-right: 8px;
+  flex-shrink: 0;
+}
+
+.http-header-value {
+  word-break: break-all;
+}
+
+.http-body-label {
+  padding: 10px;
+  background-color: var(--accent-color);
+  color: white;
+  font-weight: bold;
+}
+
+.http-body {
+  padding: 10px;
+  background-color: var(--bg-secondary);
+  white-space: pre-wrap;
+  overflow-x: auto;
+  max-height: 400px;
+  overflow-y: auto;
+}
+
+.http-body-line {
+  margin-bottom: 5px;
+  line-height: 1.5;
+}
+
+/* Toast notification styles */
+.toast-notification {
+  position: fixed;
+  bottom: 20px;
+  right: 20px;
+  max-width: 350px;
+  background-color: var(--bg-secondary);
+  color: var(--text-primary);
+  border-left: 4px solid var(--accent-color);
+  border-radius: 4px;
+  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.2);
+  transform: translateY(100px);
+  opacity: 0;
+  transition: transform 0.3s ease, opacity 0.3s ease;
+  z-index: 2000;
+}
+
+.toast-notification.show {
+  transform: translateY(0);
+  opacity: 1;
+}
+
+.toast-notification.success {
+  border-left-color: var(--button-success);
+}
+
+.toast-notification.error {
+  border-left-color: #e74c3c;
+}
+
+.toast-notification.info {
+  border-left-color: var(--accent-color);
+}
+
+.toast-content {
+  padding: 12px 15px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+}
+
+.toast-message {
+  flex: 1;
+  margin-right: 10px;
+}
+
+.toast-close {
+  background: none;
+  border: none;
+  color: var(--text-secondary);
+  font-size: 1.5rem;
+  cursor: pointer;
+  line-height: 1;
+}
+
+.toast-close:hover {
+  color: var(--text-primary);
+}
+
+/* Response indicator for 21121 events */
+.response-indicator {
+  margin-top: 5px;
+  display: flex;
+  align-items: center;
+}
+
+.response-available {
+  font-size: 0.8rem;
+  color: var(--button-success);
+  background-color: rgba(40, 167, 69, 0.1);
+  padding: 2px 8px;
+  border-radius: 4px;
+  border: 1px solid var(--button-success);
+}
+
+/* Tab style updates for HTTP content display */
+.tab-btn[data-tab="raw-http"],
+.tab-btn[data-tab="formatted-http"] {
+  font-size: 0.85rem;
+}
+
+.tab-btn[data-tab="formatted-http"].active {
+  background-color: var(--accent-color);
+  color: white;
+}
+
+.no-content {
+  padding: 20px;
+  text-align: center;
+  color: var(--text-tertiary);
+  font-style: italic;
+}
+
 /* Event item with avatar layout */
 .event-item-container {
     display: flex;
@@ -2057,4 +2594,41 @@ footer {
 
 .raw-input-container button {
     align-self: flex-start;
+}
+
+/* Decryption status indicators */
+.decryption-status {
+    margin-top: 10px;
+    padding: 8px 12px;
+    border-radius: 4px;
+    font-size: 0.9em;
+    font-weight: 500;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+.decryption-status.success {
+    background-color: rgba(40, 167, 69, 0.1);
+    color: var(--button-success);
+    border: 1px solid var(--button-success);
+}
+
+.decryption-status.error {
+    background-color: rgba(231, 76, 60, 0.1);
+    color: #e74c3c;
+    border: 1px solid #e74c3c;
+}
+
+.decryption-status::before {
+    margin-right: 8px;
+    font-weight: bold;
+}
+
+.decryption-status.success::before {
+    content: '✓';
+}
+
+.decryption-status.error::before {
+    content: '✗';
 }
\ No newline at end of file
diff --git a/client/webpack.config.js b/client/webpack.config.js
index b4c4625..e6ac767 100644
--- a/client/webpack.config.js
+++ b/client/webpack.config.js
@@ -8,6 +8,12 @@ module.exports = {
   entry: './src/client.ts',
   // Ensure webpack creates browser-compatible output
   target: 'web',
+  // Add detailed error reporting
+  stats: {
+    errorDetails: true
+  },
+  // Enable source maps for better debugging
+  devtool: 'inline-source-map',
   module: {
     rules: [
       {
@@ -28,19 +34,42 @@ module.exports = {
         { from: 'styles.css', to: 'styles.css' },
         { from: 'http.png', to: 'http.png' },
         { from: 'index.html', to: 'index.html' },
-        { from: 'help.html', to: 'help.html' },
-        { from: 'receive.html', to: 'receive.html' },
+        { from: '1120_client.html', to: '1120_client.html' },
+        { from: '1120_server.html', to: '1120_server.html' },
         { from: 'profile.html', to: 'profile.html' },
         { from: 'billboard.html', to: 'billboard.html' },
-      ],
+      ].filter(pattern => {
+        try {
+          // Only include files that exist
+          require('fs').accessSync(pattern.from);
+          return true;
+        } catch (e) {
+          console.warn(`Warning: File ${pattern.from} not found, skipping copy`);
+          return false;
+        }
+      }),
     }),
   ],
   output: {
-    filename: 'bundle.js',
+    filename: '[name].bundle.js',
+    chunkFilename: '[name].[chunkhash].js',
     path: path.resolve(__dirname, 'dist'),
+    clean: true, // Clean the output directory before emit
   },
   resolve: {
     extensions: ['.tsx', '.ts', '.js'],
+    // Add aliases for commonly used directories
+    alias: {
+      '@utils': path.resolve(__dirname, 'src/utils'),
+      '@services': path.resolve(__dirname, 'src/services'),
+    }
+  },
+  // Disable optimization and code splitting to ensure everything is in one bundle
+  optimization: {
+    // Disable code splitting entirely
+    splitChunks: false,
+    // Keep everything in one chunk
+    runtimeChunk: false
   },
   devServer: {
     static: {
@@ -48,5 +77,13 @@ module.exports = {
     },
     compress: true,
     port: 3000,
+    // Disable browser caching
+    headers: {
+      'Cache-Control': 'no-store',
+      'Pragma': 'no-cache'
+    },
+    // Hot reload settings
+    hot: true,
+    liveReload: true,
   },
 };
\ No newline at end of file
diff --git a/optimization-recommendations.md b/optimization-recommendations.md
new file mode 100644
index 0000000..dc65f38
--- /dev/null
+++ b/optimization-recommendations.md
@@ -0,0 +1,117 @@
+# Code Optimization Recommendations
+
+After analyzing the HTTP-to-Nostr converter codebase, here are recommendations for refactoring and optimizing the code to make it more maintainable and easier to work with, particularly for AI-based code assistance.
+
+## Key Issues Identified
+
+1. **Large, complex files**: Several files exceed optimal sizes for quick comprehension (NostrService.ts at 871 lines, UiService.ts at 1400+ lines).
+2. **Code duplication**: Similar patterns repeated across the codebase.
+3. **Tight coupling**: Components are tightly interlinked making isolated changes difficult.
+4. **Limited modularization**: Functionality that could be separated into utility functions is embedded in larger classes.
+
+## Optimization Recommendations
+
+### 1. File Organization and Code Splitting
+
+- **Extract utility functions into dedicated files**: 
+  - Create a `utils/` directory with specific utility files like `crypto-utils.ts`, `event-utils.ts`, `dom-utils.ts`
+  - Example: Extract WebCrypto operations from HttpService.ts into a separate `crypto-utils.ts`
+
+- **Split large files into smaller, focused modules**:
+  - Break NostrService.ts into:
+    - `NostrEventService.ts` - Event-specific operations
+    - `NostrRelayService.ts` - Relay connection management
+    - `NostrCacheService.ts` - Caching logic only
+
+- **Introduce proper layering**:
+  - Data access layer (relay communications)
+  - Business logic layer (event processing)
+  - Presentation layer (UI updates)
+
+### 2. Code Quality Improvements
+
+- **Use consistent error handling patterns**:
+  ```typescript
+  // Instead of scattered try/catch blocks with varying patterns
+  try {
+    // operation
+  } catch (error) {
+    if (error instanceof Error) {
+      console.error(`Error: ${error.message}`);
+    } else {
+      console.error(`Unknown error: ${String(error)}`);
+    }
+  }
+  ```
+
+- **Reduce code duplication**:
+  - Extract repeated WebSocket connection patterns 
+  - Create shared functions for common operations like event verification
+
+- **Improve type safety**:
+  - Avoid `any` types
+  - Add proper interfaces for all data structures
+  - Use more specific types than `Record<string, unknown>`
+
+### 3. Performance Optimizations
+
+- **Implement proper caching strategies**:
+  - More granular caching with explicit expiration policies
+  - Option to invalidate specific cache entries
+
+- **Lazy loading of components**: 
+  - Only load modules when needed
+  - Split webpack bundles for different pages
+
+- **Reduce redundant operations**:
+  - Batch relay queries where possible
+  - Improve WebSocket connection reuse
+
+### 4. Specific File Optimizations
+
+#### NostrService.ts
+
+- Extract the caching mechanism into a separate `EventCache` class (already implemented)
+- Create a utility module for repetitive operations like pubkey conversion
+- Move event query functions to a standalone `queryService`
+
+#### WebSocketManager.ts 
+
+- Implemented a more concise version with clearer error handling
+- Add connection pooling to reuse connections when possible
+
+#### HttpService.ts
+
+- Simplified version implemented with cleaner parsing and request execution
+- Extract cryptography functions to their own utility module
+
+#### UiService.ts
+
+- Break into smaller components organized by UI section
+- Separate DOM manipulation from business logic
+- Create reusable UI components for common patterns (e.g., event display)
+
+### 5. Development Workflow Improvements
+
+- **Enhanced linting rules**:
+  - Enforce consistent patterns
+  - Prevent any-type usage except where absolutely necessary
+
+- **Unit tests**:
+  - Add tests for critical functionality
+  - Mock WebSocket and crypto operations for reliable testing
+
+- **Documentation**:
+  - Add JSDoc comments for all public methods
+  - Create architecture documentation for developers
+
+## Implementation Strategy
+
+Given the complexity of the refactoring, a gradual approach is recommended:
+
+1. Start with smaller utility extractions that don't affect the overall architecture
+2. Implement the new EventCache and other stand-alone services
+3. Gradually refactor each major component while maintaining backward compatibility
+4. Update imports and replace old implementations one module at a time
+
+This approach maximizes the benefits of optimization while minimizing the risk of breaking changes.
\ No newline at end of file