# Aether Chat — LLM Reference Document > This file describes Aether Chat in detail for use by language models, AI agents, > search crawlers, and developer tooling. It covers architecture, features, data > formats, APIs, and integration points. > > Format: llms.txt (see https://llmstxt.org) ## Overview **Aether Chat** is a fully client-side, privacy-first AI chat assistant built on the [Nostr](https://nostr.com/) protocol stack. It runs entirely in the browser — no server-side backend, no user accounts, no telemetry. All conversations are stored locally in the browser's IndexedDB. - **Live URL:** - **Changelog:** - **LLM reference:** - **Repository:** nostr://npub14rg4vrt2v374q95ezeeydu3hkdhmzglcj950mggacap4x0lv0gyq04wun7/relay.ngit.dev/aether-chat-1 - **Current version:** 1.2.0 - **Built with:** [Marlowe](https://marlowe.shakespeare.wtf) — AI-powered web development platform (a Shakespeare fork) - **License:** MIT --- ## Technology Stack | Layer | Technology | |---|---| | UI framework | React 18 with hooks and concurrent rendering | | Language | TypeScript 5 (strict mode) | | Styling | TailwindCSS 3 + shadcn/ui component library | | Build tool | Vite 6 with esbuild | | Nostr integration | Nostrify (@nostrify/nostrify, @nostrify/react) | | Data fetching | TanStack Query v5 | | Routing | React Router v6 | | Local storage | IndexedDB via `idb` library | | Git (in-browser) | isomorphic-git + @isomorphic-git/lightning-fs (lazy-loaded, ngit repos) | | Markdown rendering | react-markdown + remark-gfm + rehype-highlight | | Syntax highlighting | highlight.js (github-dark theme) | | PDF export | jsPDF v4 | | QR codes | qrcode (canvas-based, pure JS) | | AI media generation | @fal-ai/client | | Font | Inter Variable (@fontsource-variable/inter) | | PWA | Custom service worker (sw.js) + Web App Manifest | --- ## Core Features ### 1. AI Chat (OpenAI-compatible APIs) Aether Chat connects to any OpenAI-compatible API endpoint. The user configures one or more API endpoints in Settings. Multiple configurations can be saved and switched between. **Supported authentication methods:** - `api-key` — Standard `Authorization: Bearer ` header - `nip98` — NIP-98 HTTP Auth using the user's Nostr signing key (no API key needed) **Supported providers (tested):** - OpenAI (api.openai.com) - Groq (api.groq.com/openai/v1) - OpenRouter (openrouter.ai/api/v1) - DeepSeek (api.deepseek.com/v1) - Mistral (api.mistral.ai/v1) - Together AI (api.together.xyz/v1) - Any other OpenAI-compatible endpoint **Request format:** Standard OpenAI `/chat/completions` with `stream: true` and `stream_options: { include_usage: true }`. Server-Sent Events (SSE) streaming is parsed chunk-by-chunk. Tool calls are accumulated from streaming deltas. **Agentic loop:** When the model requests tool calls, Aether executes them and feeds results back as `role: "tool"` messages, then calls the model again. This loop runs up to 8 rounds per user message. ### 2. Tools / Function Calling The following tools are exposed to the model and executed client-side: | Tool name | Trigger | Implementation | |---|---|---| | `web_search` | User asks about current events/facts | Bing RSS feed via CORS proxy; Wikipedia fallback | | `nostr_search` | User asks to search Nostr | NIP-50 full-text search on relay group | | `read_url` | User shares a URL / model needs page content | GitHub + GitLab REST APIs; NIP-34 relays + in-browser git clone; CORS proxy for generic pages | | `generate_image` | User asks to create/draw an image | fal.ai REST API (`@fal-ai/client`) | | `generate_video` | User asks to create a video | fal.ai REST API | | `deep_research` | User asks for deep research | Multi-step: plan → search loop → synthesise | **URL auto-fetch:** Independently of the tool, any `http(s)://` or `nostr://` URL in a user message is detected automatically, fetched (max 3 per message), and injected into the API context as a synthetic user message. Only the visible step messages are persisted in the chat — the fetched content itself is not stored. Each tool execution emits one or more **step messages** — persistent `assistant` messages stored in IndexedDB with a `stepType` field — so the user sees every step of the process in the chat timeline. ### 3. Web Search **Primary:** Bing RSS feed (`https://www.bing.com/search?q=...&format=rss`) - No API key required - No bot detection (RSS is served to automated readers) - Proxied through `https://proxy.shakespeare.diy/?url=` - Returns title, URL, description snippet **Fallback:** Wikipedia OpenSearch API - `https://en.wikipedia.org/w/api.php?action=query&list=search&...&origin=*` - CORS-native, no proxy needed - Used when Bing RSS fails or returns empty ### 4. Nostr Search (NIP-50) Nostr full-text search uses the NIP-50 `search` filter field on search-capable relays. **Search relays:** - `wss://search.nos.today` - `wss://nostr.wine` - `wss://relay.noswhere.com` - `wss://gleasonator.dev/relay` Query: `[{ kinds: [1], search: "", limit: 5 }]` **NIP-19 identifier lookup:** When the search query begins with `npub1`, `note1`, `nevent1`, `nprofile1`, or `naddr1`, Aether decodes the identifier using `nip19.decode()` from nostr-tools and performs a direct relay lookup instead: | Identifier | Filter used | |---|---| | `npub1` / `nprofile1` | `{ kinds: [0, 1], authors: [pubkey] }` | | `note1` | `{ ids: [eventId] }` | | `nevent1` | `{ ids: [eventId] }` | | `naddr1` | `{ kinds: [kind], authors: [pubkey], '#d': [identifier] }` | When a NIP-19 lookup is used, the AI is automatically instructed to summarise the retrieved content for the user. ### 5. URL Reader & Git Repositories The `read_url` tool (and automatic URL detection) understands several URL classes, dispatched by host/path pattern in `src/hooks/useUrlReader.ts`: **GitHub** (`github.com`, `raw.githubusercontent.com`) — uses the public GitHub REST API (CORS-native, no key required, 60 req/hr per IP): - Repo root → metadata (description, stars, language, topics, license) + README (`/repos/{o}/{r}/readme` with raw Accept header) + recursive file tree (`/git/trees/{branch}?recursive=1`, first 150 files) - `/blob/{branch}/{path}` → raw file from `raw.githubusercontent.com` (branch names containing slashes resolved by trying up to 3 prefix splits) - `/tree/{branch}/{path}` → directory listing via the Contents API **GitLab** (`gitlab.com` bare paths; any host with `/-/blob` or `/-/tree` URLs, including self-hosted instances) — uses the GitLab v4 API (CORS-native): - Repo root → project metadata + README (tries README.md/readme.md/README/README.rst) + recursive tree (first 150 files) - `/-/blob/{branch}/{path}` → raw file via `/repository/files/{path}/raw` - `/-/tree/{branch}/{path}` → directory listing via `/repository/tree` **ngit / NIP-34** (`nostr://` URLs and `gitworkshop.dev/{npub|nip05}/{repo}`): - Parses `nostr:////` and `nostr://` (NIP-05 identifiers resolved via `/.well-known/nostr.json`) - Queries the relay hint (plus `relay.ngit.dev`, `relay.damus.io`, `relay.primal.net`, `nos.lol`) for the kind **30617** repository announcement (name, description, web, clone, relays, maintainers, hashtags) and the kind **30618** state event for the default branch (`HEAD` tag) - Then **shallow-clones the repo in the browser** with isomorphic-git (depth 1, single branch) from the first `https://` clone URL — grasp servers allow CORS `*` — and extracts the file list at HEAD plus the README. The clone goes to an ephemeral lightning-fs (IndexedDB) directory that is wiped after reading. If cloning fails, the announcement metadata alone is returned with a note. **Generic web pages** — fetched via the CORS proxy; HTML is parsed with DOMParser (scripts/styles/nav/header/footer stripped), title/description from `` and OpenGraph tags, body text collapsed and truncated to ~6000 chars. Non-HTML textual content (JSON, plain text, markdown, XML) is returned raw. Binary content (images, PDFs, etc.) is rejected with a clear error. Content limits: README 4000 chars, files 6000 chars, pages 6000 chars, trees 150 entries. The UI shows `read_url` / `git_repo` step rows in the chat timeline. ### 6. AI Media Generation (fal.ai) Image and video generation uses the fal.ai platform via `@fal-ai/client`. **Authentication:** fal.ai API key (stored in settings, never sent to any server except fal.ai). NIP-98 auth is listed as an option but fal.ai uses their own key system. **Default image models:** - `fal-ai/flux/schnell` (fast) - `fal-ai/flux/dev` (quality) - `fal-ai/flux-pro`, `fal-ai/flux-pro/v1.1` - `fal-ai/stable-diffusion-v3-medium` - `fal-ai/aura-flow`, `fal-ai/hyper-sdxl` - Custom model ID input available for any fal.ai model **Default video models:** - `fal-ai/kling-video/v1/standard/text-to-video` - `fal-ai/kling-video/v1.6/standard/text-to-video` - `fal-ai/minimax-video/image-to-video` - `fal-ai/ltx-video`, `fal-ai/cogvideox-5b` - Custom model ID input available Generated media is attached to step messages and the final answer message. Images are rendered inline in the chat. Videos get a `<video>` element. ### 7. Deep Research Multi-step research pipeline triggered by the `deep_research` tool: 1. **Planning:** Ask the model to generate N search queries for the topic 2. **Search loop:** For each query, run `web_search` + `nostr_search` in parallel 3. **Synthesis:** Ask the model to write a comprehensive report from all findings Each step is persisted as a visible `assistant` message with `stepType` set to `research_plan`, `research_search`, or `research_synthesize`. Max iterations configurable (default 5, max 8). ### 8. PDF Export Exports any chat conversation to a styled PDF using jsPDF. **Features:** - 6 randomly-assigned colour schemes per export (Ocean Depths, Midnight Aurora, Forest Dawn, Crimson Ember, Steel Horizon, Rose Gold) - Colour-coded chat bubbles: user vs assistant, with accent bar - Model name and timestamp on each message - Token usage per message - Emoji rendering via canvas-to-PNG pipeline (each emoji rendered to a 80px canvas, embedded as PNG — jsPDF has no emoji font support) - Multi-page support: bubble backgrounds span pages correctly (band drawn before text to avoid z-order issues) - Embedded images (generated by fal.ai or attached) - QR codes for video URLs and HTTP(S) links found in message text (via qrcode library, canvas-rendered, 28mm square) - Step messages rendered as compact tinted rows with bold label prefix - Page header with title, model name, date, message count - Page footer with page numbers on every page ### 9. Personalization Users can configure how the AI presents itself and responds: - **User name** — AI addresses the user by name - **About you** — Background, interests, occupation fed into the system prompt - **Assistant name** — Defaults to "Aether" - **AI personality/tone** — Free text (e.g. "helpful, concise, slightly witty") - **Preferred language** — 23 languages supported; AI responds in chosen language - **Custom system prompt** — Additional instructions appended to the system prompt ### 10. Settings Storage Settings can be stored in two modes (user's choice): - **Local** (default) — `localStorage` key `aether-chat-settings`, stays in browser - **Nostr NIP-78** — Encrypted with NIP-44 (self-encryption to user's own pubkey), published as kind `30078` with `d` tag `aether-chat:settings:v1`. Can be loaded from any device where the user is logged in with the same Nostr key. **Security note:** API keys are included in settings. When using NIP-78 storage, they are encrypted with NIP-44 before being published to relays. **Chats are never uploaded to Nostr** without explicit user action. Chat history remains in IndexedDB only. --- ## Pages & Routes | Route | Description | |---|---| | `/` | Main chat interface (ChatProvider → ChatLayout) | | `/changelog` | Full release history and version changelog | | `/llms.txt` | This document (served as static file from /public) | | `/:nip19` | NIP-19 identifier routing (npub1, note1, nevent1, nprofile1, naddr1) | --- ## Data Model ### ChatMessage ```typescript interface ChatMessage { id: string; // "msg_<timestamp>_<random>" role: 'user' | 'assistant' | 'system' | 'tool'; content: string; // Markdown text status: 'pending' | 'streaming' | 'done' | 'error'; createdAt: number; // Unix ms model?: string; // e.g. "gpt-4o-mini" attachments?: MediaAttachment[]; toolCalls?: ToolCall[]; toolsUsed?: ToolUseRecord[]; // Chips shown below the message toolCallId?: string; // For role:'tool' messages tokens?: { prompt: number; completion: number; total: number }; error?: string; stepType?: StepType; // If set, renders as a compact step row stepParentId?: string; // ID of the final-answer message this step belongs to } ``` ### StepType Step messages are assistant messages with a `stepType` field. They are: - Stored permanently in IndexedDB - Shown in the chat as compact timeline rows (not full bubbles) - Excluded from the context sent to the AI API - Shown in PDF exports as tinted compact rows ```typescript type StepType = | 'web_search' | 'nostr_search' | 'read_url' | 'git_repo' | 'generate_image' | 'generate_video' | 'research_plan' | 'research_search' | 'research_synthesize' | 'progress'; ``` ### Chat ```typescript interface Chat { id: string; // "chat_<timestamp>_<random>" title: string; // Auto-generated from first user message createdAt: number; updatedAt: number; messages: ChatMessage[]; model?: string; pinned?: boolean; } ``` ### APIConfig ```typescript interface APIConfig { id: string; name: string; baseUrl: string; // e.g. "https://api.openai.com/v1" authMethod: 'api-key' | 'nip98'; apiKey?: string; model: string; availableModels?: string[]; // Cached from /models endpoint maxTokens?: number; temperature?: number; topP?: number; } ``` --- ## Nostr Integration Aether Chat is built on the Nostr protocol and integrates with it in several ways: ### Authentication Users can log in with their Nostr identity via: - NIP-07 browser extension (Alby, nos2x, etc.) - `nsec` private key - NIP-46 remote signer ("bunker://") ### NIP-98 HTTP Auth When an API config uses `authMethod: 'nip98'`, each request to the AI API is authenticated with a NIP-98 token — a signed Nostr event (kind 27235) containing the request URL and method, base64-encoded and sent as `Authorization: Nostr <token>`. This allows using Nostr identity as the API credential without a traditional API key. ### NIP-50 Search Nostr content search uses the NIP-50 `search` filter field on supporting relays. The query is sent as a standard Nostr REQ filter: `{ "search": "<query>" }`. ### NIP-78 Settings Sync Application settings can optionally be synced to Nostr as kind `30078` addressable events. The `content` field is encrypted with NIP-44 (encrypt-to-self) before publishing. The `d` tag is `aether-chat:settings:v1`. ### NIP-19 Lookups The Nostr search tool recognises NIP-19 bech32 identifiers (npub1, note1, nevent1, nprofile1, naddr1) and resolves them to actual Nostr events via relay queries, then asks the AI to summarise the content. --- ## UI Structure ``` / (Index page) └── ChatProvider (context) ├── ChatSidebar │ ├── New Chat button │ ├── Search input │ ├── Chat list (pinned + recent) │ ├── LoginArea (Nostr auth) │ ├── Settings button │ ├── Changelog link (v1.2.0) │ └── Edit with Marlowe badge ├── ChatHeader │ ├── Sidebar toggle │ ├── Chat title │ ├── Export PDF button │ ├── Dark/light toggle │ └── Settings button ├── ChatArea │ ├── WelcomeScreen (no active chat) │ ├── EmptyChat (new chat, no messages) │ ├── MessageList │ │ ├── MessageBubble (user/assistant) │ │ └── StepBubble (tool step messages) │ └── ChatInput │ ├── Quick-action toggle buttons ×6 │ ├── Textarea │ └── Send / Stop button └── SettingsPanel (dialog) ├── Tab: API (OpenAI endpoints) ├── Tab: Media (fal.ai) ├── Tab: Persona (personalization) ├── Tab: Tools (web/nostr search, URL reader, research) └── Tab: More (display, storage, NIP-78 sync) /changelog └── Changelog page — full release history with version badges and timeline /:nip19 └── NIP19Page — handles npub1, note1, nevent1, nprofile1, naddr1 ``` --- ## Quick-Action Buttons Six toggle buttons above the chat input prepend a prefix to the user's message, signalling to the AI which tool to use: | Button | Prefix inserted | Tool triggered | |---|---|---| | Web Search | `Search the web for: ` | `web_search` | | Nostr Search | `Search Nostr for: ` | `nostr_search` | | Fetch URL | `Read this URL: ` | `read_url` (+ URL auto-fetch) | | Generate Image | `Generate an image of: ` | `generate_image` | | Generate Video | `Generate a video of: ` | `generate_video` | | Deep Research | `Do deep research on: ` | `deep_research` | Buttons toggle on/off. Clicking an active button removes the prefix. Switching between buttons swaps prefixes. If the user manually deletes the prefix, the button deactivates automatically. --- ## Progressive Web App (PWA) Aether Chat is installable as a PWA on all major platforms. **Manifest:** `/manifest.webmanifest` - `display: standalone` - `theme_color: #8b5cf6` (violet) - `background_color: #0d1117` (near-black) - Icons: 192px (any), 512px (any), 512px maskable, 180px Apple touch - Shortcut: "New Chat" **Service Worker:** `/sw.js` - Precaches: index.html, manifest, icons - Navigation requests: network-first, falls back to /index.html (SPA routing) - Static assets: cache-first - API/WebSocket calls: not intercepted (pass-through) - Updates: `skipWaiting` + `clientsClaim` for immediate activation --- ## Privacy & Security - **No server:** The app is entirely static HTML/JS/CSS. No backend, no database, no user accounts. - **Local-first:** All chat history lives in the user's browser IndexedDB. Clearing browser data deletes all chats. - **API keys:** Stored in `localStorage`, transmitted only to the configured API endpoint. Never sent to any Aether/Shakespeare server. - **Nostr login:** Private keys never leave the signer (extension or in-memory). NIP-07, NIP-46, and nsec login methods are supported. - **NIP-78 encryption:** When syncing settings to Nostr, the entire settings object (including API keys) is encrypted with NIP-44 before publishing. - **Chats:** Never uploaded anywhere. Explicit opt-in required for any Nostr sync, and no chat-sync feature exists — only settings sync is implemented. - **CORS proxy:** External search requests and generic URL reads are routed through `https://proxy.shakespeare.diy/?url=<encoded>` to bypass browser CORS restrictions. This proxy is operated by Shakespeare and sees the requested URLs but not the chat content. GitHub/GitLab API calls, Nostr relay traffic, and in-browser git clones go directly to their respective servers without a proxy. --- ## File Layout ``` / ├── index.html # Entry point with all meta/OG/PWA tags ├── package.json # Version number (semver) ├── public/ │ ├── manifest.webmanifest # PWA manifest │ ├── sw.js # Service worker │ ├── llms.txt # This document (llms.txt standard) │ ├── og-image.jpg # OpenGraph image (1536×1024) │ ├── icon-512.png # App icon 512px │ ├── icon-192.png # App icon 192px │ ├── icon-maskable-512.png # Maskable icon (Android adaptive) │ ├── apple-touch-icon.png # iOS home screen icon (180px) │ ├── icon-96.png # Shortcut icon │ ├── favicon-32x32.png # Browser tab favicon │ ├── favicon-16x16.png # Browser tab favicon (legacy) │ ├── robots.txt │ └── _redirects # Netlify SPA redirect rule └── src/ ├── main.tsx # Entry, SW registration ├── App.tsx # Root providers ├── AppRouter.tsx # React Router routes ├── index.css # Tailwind + custom CSS ├── contexts/ │ ├── ChatContext.tsx # Chat state, IDB operations │ ├── AppContext.ts # Theme, relay config │ ├── NWCContext.tsx # Nostr Wallet Connect │ └── DMContext.ts # Direct messages ├── hooks/ │ ├── useSendMessage.ts # Main agentic loop (+ URL auto-fetch) │ ├── useChatCompletion.ts# OpenAI streaming client │ ├── useDeepResearch.ts # Multi-step research │ ├── useWebSearch.ts # Bing RSS + NIP-50 search │ ├── useUrlReader.ts # URL reader: web, GitHub, GitLab, ngit/NIP-34 │ ├── useFalAI.ts # fal.ai image/video generation │ ├── useNip98Auth.ts # NIP-98 token generation │ ├── useNip78Settings.ts # NIP-78 settings sync │ └── useCurrentUser.ts # Nostr login state ├── components/ │ ├── chat/ │ │ ├── ChatArea.tsx # Message list + input container │ │ ├── ChatHeader.tsx # Top bar with actions │ │ ├── ChatInput.tsx # Message input + quick actions │ │ ├── ChatSidebar.tsx # Chat history list + nav links │ │ ├── MessageBubble.tsx # User/assistant messages + StepBubble │ │ └── SettingsPanel.tsx # Settings dialog (5 tabs) │ ├── auth/ # LoginArea, LoginDialog │ └── ui/ # shadcn/ui components └── lib/ ├── chatTypes.ts # All TypeScript types ├── chatStorage.ts # IndexedDB via idb library └── pdfExport.ts # jsPDF export with QR codes └── pages/ ├── Index.tsx # Main chat page ├── Changelog.tsx # Version history at /changelog ├── NIP19Page.tsx # NIP-19 identifier routing └── NotFound.tsx # 404 page ``` --- ## Versioning Aether Chat follows [Semantic Versioning](https://semver.org/): - **MAJOR** (x.0.0): Breaking changes or full redesigns - **MINOR** (1.x.0): New features, new pages, new integrations - **PATCH** (1.1.x): Bug fixes, copy changes, minor tweaks The canonical version is in `package.json` → `version`. The changelog at `/changelog` (source: `src/pages/Changelog.tsx`) lists every release. --- ## Environment & Deployment - **No environment variables required** — all configuration is done by the user at runtime through the Settings UI - **Static hosting** — deploy the `dist/` folder to any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages, nsite, etc.) - **SPA routing** — `_redirects` file handles Netlify; `404.html` copy of `index.html` handles other hosts - **Deployed at:** <https://aether-chat.shakespeare.wtf> --- ## Extending / Integrating ### Adding a new tool 1. Define an `OpenAITool` constant in `useSendMessage.ts` 2. Add a case to `executeTool()` that calls `emitStep()` for progress and returns `{ result: string, record: ToolUseRecord }` 3. Add the tool to the `tools[]` array inside `sendMessage()` 4. Optionally add a `StepType` variant in `chatTypes.ts` and a row in `STEP_META` in `MessageBubble.tsx` and `STEP_LABELS` in `pdfExport.ts` ### Adding a new API provider Add a preset to the "Popular APIs" section in `SettingsPanel.tsx`: ```typescript { name: 'MyProvider', url: 'https://api.myprovider.com/v1', model: 'my-model' } ``` The provider must implement the OpenAI `/chat/completions` endpoint with streaming. ### Using NIP-98 with a custom server Implement the NIP-98 verification on your server: 1. Parse `Authorization: Nostr <base64>` header 2. Decode base64 → JSON event 3. Verify the Nostr event signature 4. Check `kind === 27235`, tag `u` matches request URL, `method` matches HTTP method 5. Check `created_at` is within ±60 seconds of current time --- *Updated: 2026-08-04 | Aether Chat v1.2.0 | Built with [Marlowe](https://marlowe.shakespeare.wtf)*