diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index ccbaec8..e24b021 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -15,7 +15,10 @@
"Bash(./scripts/test.sh:*)",
"Bash(./scripts/update-embedded-web.sh:*)",
"Bash(bun run build:*)",
- "Bash(bun update:*)"
+ "Bash(bun update:*)",
+ "Bash(cat:*)",
+ "Bash(git add:*)",
+ "Bash(git commit:*)"
],
"deny": [],
"ask": []
diff --git a/.claude/skills/applesauce-core/SKILL.md b/.claude/skills/applesauce-core/SKILL.md
new file mode 100644
index 0000000..7830711
--- /dev/null
+++ b/.claude/skills/applesauce-core/SKILL.md
@@ -0,0 +1,634 @@
+---
+name: applesauce-core
+description: This skill should be used when working with applesauce-core library for Nostr client development, including event stores, queries, observables, and client utilities. Provides comprehensive knowledge of applesauce patterns for building reactive Nostr applications.
+---
+
+# applesauce-core Skill
+
+This skill provides comprehensive knowledge and patterns for working with applesauce-core, a library that provides reactive utilities and patterns for building Nostr clients.
+
+## When to Use This Skill
+
+Use this skill when:
+- Building reactive Nostr applications
+- Managing event stores and caches
+- Working with observable patterns for Nostr
+- Implementing real-time updates
+- Building timeline and feed views
+- Managing replaceable events
+- Working with profiles and metadata
+- Creating efficient Nostr queries
+
+## Core Concepts
+
+### applesauce-core Overview
+
+applesauce-core provides:
+- **Event stores** - Reactive event caching and management
+- **Queries** - Declarative event querying patterns
+- **Observables** - RxJS-based reactive patterns
+- **Profile helpers** - Profile metadata management
+- **Timeline utilities** - Feed and timeline building
+- **NIP helpers** - NIP-specific utilities
+
+### Installation
+
+```bash
+npm install applesauce-core
+```
+
+### Basic Architecture
+
+applesauce-core is built on reactive principles:
+- Events are stored in reactive stores
+- Queries return observables that update when new events arrive
+- Components subscribe to observables for real-time updates
+
+## Event Store
+
+### Creating an Event Store
+
+```javascript
+import { EventStore } from 'applesauce-core';
+
+// Create event store
+const eventStore = new EventStore();
+
+// Add events
+eventStore.add(event1);
+eventStore.add(event2);
+
+// Add multiple events
+eventStore.addMany([event1, event2, event3]);
+
+// Check if event exists
+const exists = eventStore.has(eventId);
+
+// Get event by ID
+const event = eventStore.get(eventId);
+
+// Remove event
+eventStore.remove(eventId);
+
+// Clear all events
+eventStore.clear();
+```
+
+### Event Store Queries
+
+```javascript
+// Get all events
+const allEvents = eventStore.getAll();
+
+// Get events by filter
+const filtered = eventStore.filter({
+ kinds: [1],
+ authors: [pubkey]
+});
+
+// Get events by author
+const authorEvents = eventStore.getByAuthor(pubkey);
+
+// Get events by kind
+const textNotes = eventStore.getByKind(1);
+```
+
+### Replaceable Events
+
+applesauce-core handles replaceable events automatically:
+
+```javascript
+// For kind 0 (profile), only latest is kept
+eventStore.add(profileEvent1); // stored
+eventStore.add(profileEvent2); // replaces if newer
+
+// For parameterized replaceable (30000-39999)
+eventStore.add(articleEvent); // keyed by author + kind + d-tag
+
+// Get replaceable event
+const profile = eventStore.getReplaceable(0, pubkey);
+const article = eventStore.getReplaceable(30023, pubkey, 'article-slug');
+```
+
+## Queries
+
+### Query Patterns
+
+```javascript
+import { createQuery } from 'applesauce-core';
+
+// Create a query
+const query = createQuery(eventStore, {
+ kinds: [1],
+ limit: 50
+});
+
+// Subscribe to query results
+query.subscribe(events => {
+ console.log('Current events:', events);
+});
+
+// Query updates automatically when new events added
+eventStore.add(newEvent); // Subscribers notified
+```
+
+### Timeline Query
+
+```javascript
+import { TimelineQuery } from 'applesauce-core';
+
+// Create timeline for user's notes
+const timeline = new TimelineQuery(eventStore, {
+ kinds: [1],
+ authors: [userPubkey]
+});
+
+// Get observable of timeline
+const timeline$ = timeline.events$;
+
+// Subscribe
+timeline$.subscribe(events => {
+ // Events sorted by created_at, newest first
+ renderTimeline(events);
+});
+```
+
+### Profile Query
+
+```javascript
+import { ProfileQuery } from 'applesauce-core';
+
+// Query profile metadata
+const profileQuery = new ProfileQuery(eventStore, pubkey);
+
+// Get observable
+const profile$ = profileQuery.profile$;
+
+profile$.subscribe(profile => {
+ if (profile) {
+ console.log('Name:', profile.name);
+ console.log('Picture:', profile.picture);
+ }
+});
+```
+
+## Observables
+
+### Working with RxJS
+
+applesauce-core uses RxJS observables:
+
+```javascript
+import { map, filter, distinctUntilChanged } from 'rxjs/operators';
+
+// Transform query results
+const names$ = profileQuery.profile$.pipe(
+ filter(profile => profile !== null),
+ map(profile => profile.name),
+ distinctUntilChanged()
+);
+
+// Combine multiple observables
+import { combineLatest } from 'rxjs';
+
+const combined$ = combineLatest([
+ timeline$,
+ profile$
+]).pipe(
+ map(([events, profile]) => ({
+ events,
+ authorName: profile?.name
+ }))
+);
+```
+
+### Creating Custom Observables
+
+```javascript
+import { Observable } from 'rxjs';
+
+function createEventObservable(store, filter) {
+ return new Observable(subscriber => {
+ // Initial emit
+ subscriber.next(store.filter(filter));
+
+ // Subscribe to store changes
+ const unsubscribe = store.onChange(() => {
+ subscriber.next(store.filter(filter));
+ });
+
+ // Cleanup
+ return () => unsubscribe();
+ });
+}
+```
+
+## Profile Helpers
+
+### Profile Metadata
+
+```javascript
+import { parseProfile, ProfileContent } from 'applesauce-core';
+
+// Parse kind 0 content
+const profileEvent = await getProfileEvent(pubkey);
+const profile = parseProfile(profileEvent);
+
+// Profile fields
+console.log(profile.name); // Display name
+console.log(profile.about); // Bio
+console.log(profile.picture); // Avatar URL
+console.log(profile.banner); // Banner image URL
+console.log(profile.nip05); // NIP-05 identifier
+console.log(profile.lud16); // Lightning address
+console.log(profile.website); // Website URL
+```
+
+### Profile Store
+
+```javascript
+import { ProfileStore } from 'applesauce-core';
+
+const profileStore = new ProfileStore(eventStore);
+
+// Get profile observable
+const profile$ = profileStore.getProfile(pubkey);
+
+// Get multiple profiles
+const profiles$ = profileStore.getProfiles([pubkey1, pubkey2]);
+
+// Request profile load (triggers fetch if not cached)
+profileStore.requestProfile(pubkey);
+```
+
+## Timeline Utilities
+
+### Building Feeds
+
+```javascript
+import { Timeline } from 'applesauce-core';
+
+// Create timeline
+const timeline = new Timeline(eventStore);
+
+// Add filter
+timeline.setFilter({
+ kinds: [1, 6],
+ authors: followedPubkeys
+});
+
+// Get events observable
+const events$ = timeline.events$;
+
+// Load more (pagination)
+timeline.loadMore(50);
+
+// Refresh (get latest)
+timeline.refresh();
+```
+
+### Thread Building
+
+```javascript
+import { ThreadBuilder } from 'applesauce-core';
+
+// Build thread from root event
+const thread = new ThreadBuilder(eventStore, rootEventId);
+
+// Get thread observable
+const thread$ = thread.thread$;
+
+thread$.subscribe(threadData => {
+ console.log('Root:', threadData.root);
+ console.log('Replies:', threadData.replies);
+ console.log('Reply count:', threadData.replyCount);
+});
+```
+
+### Reactions and Zaps
+
+```javascript
+import { ReactionStore, ZapStore } from 'applesauce-core';
+
+// Reactions
+const reactionStore = new ReactionStore(eventStore);
+const reactions$ = reactionStore.getReactions(eventId);
+
+reactions$.subscribe(reactions => {
+ console.log('Likes:', reactions.likes);
+ console.log('Custom:', reactions.custom);
+});
+
+// Zaps
+const zapStore = new ZapStore(eventStore);
+const zaps$ = zapStore.getZaps(eventId);
+
+zaps$.subscribe(zaps => {
+ console.log('Total sats:', zaps.totalAmount);
+ console.log('Zap count:', zaps.count);
+});
+```
+
+## NIP Helpers
+
+### NIP-05 Verification
+
+```javascript
+import { verifyNip05 } from 'applesauce-core';
+
+// Verify NIP-05
+const result = await verifyNip05('alice@example.com', expectedPubkey);
+
+if (result.valid) {
+ console.log('NIP-05 verified');
+} else {
+ console.log('Verification failed:', result.error);
+}
+```
+
+### NIP-10 Reply Parsing
+
+```javascript
+import { parseReplyTags } from 'applesauce-core';
+
+// Parse reply structure
+const parsed = parseReplyTags(event);
+
+console.log('Root event:', parsed.root);
+console.log('Reply to:', parsed.reply);
+console.log('Mentions:', parsed.mentions);
+```
+
+### NIP-65 Relay Lists
+
+```javascript
+import { parseRelayList } from 'applesauce-core';
+
+// Parse relay list event (kind 10002)
+const relays = parseRelayList(relayListEvent);
+
+console.log('Read relays:', relays.read);
+console.log('Write relays:', relays.write);
+```
+
+## Integration with nostr-tools
+
+### Using with SimplePool
+
+```javascript
+import { SimplePool } from 'nostr-tools';
+import { EventStore } from 'applesauce-core';
+
+const pool = new SimplePool();
+const eventStore = new EventStore();
+
+// Load events into store
+pool.subscribeMany(relays, [filter], {
+ onevent(event) {
+ eventStore.add(event);
+ }
+});
+
+// Query store reactively
+const timeline$ = createTimelineQuery(eventStore, filter);
+```
+
+### Publishing Events
+
+```javascript
+import { finalizeEvent } from 'nostr-tools';
+
+// Create event
+const event = finalizeEvent({
+ kind: 1,
+ content: 'Hello!',
+ created_at: Math.floor(Date.now() / 1000),
+ tags: []
+}, secretKey);
+
+// Add to local store immediately (optimistic update)
+eventStore.add(event);
+
+// Publish to relays
+await pool.publish(relays, event);
+```
+
+## Svelte Integration
+
+### Using in Svelte Components
+
+```svelte
+
+
+{#each events as event}
+
+ {event.content}
+
+{/each}
+```
+
+### Svelte Store Adapter
+
+```javascript
+import { readable } from 'svelte/store';
+
+// Convert RxJS observable to Svelte store
+function fromObservable(observable, initialValue) {
+ return readable(initialValue, set => {
+ const subscription = observable.subscribe(set);
+ return () => subscription.unsubscribe();
+ });
+}
+
+// Usage
+const events$ = timeline.events$;
+const eventsStore = fromObservable(events$, []);
+```
+
+```svelte
+
+
+{#each $eventsStore as event}
+ {event.content}
+{/each}
+```
+
+## Best Practices
+
+### Store Management
+
+1. **Single store instance** - Use one EventStore per app
+2. **Clear stale data** - Implement cache limits
+3. **Handle replaceable events** - Let store manage deduplication
+4. **Unsubscribe** - Clean up subscriptions on component destroy
+
+### Query Optimization
+
+1. **Use specific filters** - Narrow queries perform better
+2. **Limit results** - Use limit for initial loads
+3. **Cache queries** - Reuse query instances
+4. **Debounce updates** - Throttle rapid changes
+
+### Memory Management
+
+1. **Limit store size** - Implement LRU or time-based eviction
+2. **Clean up observables** - Unsubscribe when done
+3. **Use weak references** - For profile caches
+4. **Paginate large feeds** - Don't load everything at once
+
+### Reactive Patterns
+
+1. **Prefer observables** - Over imperative queries
+2. **Use operators** - Transform data with RxJS
+3. **Combine streams** - For complex views
+4. **Handle loading states** - Show placeholders
+
+## Common Patterns
+
+### Event Deduplication
+
+```javascript
+// EventStore handles deduplication automatically
+eventStore.add(event1);
+eventStore.add(event1); // No duplicate
+
+// For manual deduplication
+const seen = new Set();
+events.filter(e => {
+ if (seen.has(e.id)) return false;
+ seen.add(e.id);
+ return true;
+});
+```
+
+### Optimistic Updates
+
+```javascript
+async function publishNote(content) {
+ // Create event
+ const event = await createEvent(content);
+
+ // Add to store immediately (optimistic)
+ eventStore.add(event);
+
+ try {
+ // Publish to relays
+ await pool.publish(relays, event);
+ } catch (error) {
+ // Remove on failure
+ eventStore.remove(event.id);
+ throw error;
+ }
+}
+```
+
+### Loading States
+
+```javascript
+import { BehaviorSubject, combineLatest } from 'rxjs';
+
+const loading$ = new BehaviorSubject(true);
+const events$ = timeline.events$;
+
+const state$ = combineLatest([loading$, events$]).pipe(
+ map(([loading, events]) => ({
+ loading,
+ events,
+ empty: !loading && events.length === 0
+ }))
+);
+
+// Start loading
+loading$.next(true);
+await loadEvents();
+loading$.next(false);
+```
+
+### Infinite Scroll
+
+```javascript
+function createInfiniteScroll(timeline, pageSize = 50) {
+ let loading = false;
+
+ async function loadMore() {
+ if (loading) return;
+
+ loading = true;
+ await timeline.loadMore(pageSize);
+ loading = false;
+ }
+
+ function onScroll(event) {
+ const { scrollTop, scrollHeight, clientHeight } = event.target;
+ if (scrollHeight - scrollTop <= clientHeight * 1.5) {
+ loadMore();
+ }
+ }
+
+ return { loadMore, onScroll };
+}
+```
+
+## Troubleshooting
+
+### Common Issues
+
+**Events not updating:**
+- Check subscription is active
+- Verify events are being added to store
+- Ensure filter matches events
+
+**Memory growing:**
+- Implement store size limits
+- Clean up subscriptions
+- Use weak references where appropriate
+
+**Slow queries:**
+- Add indexes for common queries
+- Use more specific filters
+- Implement pagination
+
+**Stale data:**
+- Implement refresh mechanisms
+- Set up real-time subscriptions
+- Handle replaceable event updates
+
+## References
+
+- **applesauce GitHub**: https://github.com/hzrd149/applesauce
+- **RxJS Documentation**: https://rxjs.dev
+- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools
+- **Nostr Protocol**: https://github.com/nostr-protocol/nostr
+
+## Related Skills
+
+- **nostr-tools** - Lower-level Nostr operations
+- **applesauce-signers** - Event signing abstractions
+- **svelte** - Building reactive UIs
+- **nostr** - Nostr protocol fundamentals
diff --git a/.claude/skills/applesauce-signers/SKILL.md b/.claude/skills/applesauce-signers/SKILL.md
new file mode 100644
index 0000000..40d07e6
--- /dev/null
+++ b/.claude/skills/applesauce-signers/SKILL.md
@@ -0,0 +1,757 @@
+---
+name: applesauce-signers
+description: This skill should be used when working with applesauce-signers library for Nostr event signing, including NIP-07 browser extensions, NIP-46 remote signing, and custom signer implementations. Provides comprehensive knowledge of signing patterns and signer abstractions.
+---
+
+# applesauce-signers Skill
+
+This skill provides comprehensive knowledge and patterns for working with applesauce-signers, a library that provides signing abstractions for Nostr applications.
+
+## When to Use This Skill
+
+Use this skill when:
+- Implementing event signing in Nostr applications
+- Integrating with NIP-07 browser extensions
+- Working with NIP-46 remote signers
+- Building custom signer implementations
+- Managing signing sessions
+- Handling signing requests and permissions
+- Implementing multi-signer support
+
+## Core Concepts
+
+### applesauce-signers Overview
+
+applesauce-signers provides:
+- **Signer abstraction** - Unified interface for different signers
+- **NIP-07 integration** - Browser extension support
+- **NIP-46 support** - Remote signing (Nostr Connect)
+- **Simple signers** - Direct key signing
+- **Permission handling** - Manage signing requests
+- **Observable patterns** - Reactive signing states
+
+### Installation
+
+```bash
+npm install applesauce-signers
+```
+
+### Signer Interface
+
+All signers implement a common interface:
+
+```typescript
+interface Signer {
+ // Get public key
+ getPublicKey(): Promise;
+
+ // Sign event
+ signEvent(event: UnsignedEvent): Promise;
+
+ // Encrypt (NIP-04)
+ nip04Encrypt?(pubkey: string, plaintext: string): Promise;
+ nip04Decrypt?(pubkey: string, ciphertext: string): Promise;
+
+ // Encrypt (NIP-44)
+ nip44Encrypt?(pubkey: string, plaintext: string): Promise;
+ nip44Decrypt?(pubkey: string, ciphertext: string): Promise;
+}
+```
+
+## Simple Signer
+
+### Using Secret Key
+
+```javascript
+import { SimpleSigner } from 'applesauce-signers';
+import { generateSecretKey } from 'nostr-tools';
+
+// Create signer with existing key
+const signer = new SimpleSigner(secretKey);
+
+// Or generate new key
+const newSecretKey = generateSecretKey();
+const newSigner = new SimpleSigner(newSecretKey);
+
+// Get public key
+const pubkey = await signer.getPublicKey();
+
+// Sign event
+const unsignedEvent = {
+ kind: 1,
+ content: 'Hello Nostr!',
+ created_at: Math.floor(Date.now() / 1000),
+ tags: []
+};
+
+const signedEvent = await signer.signEvent(unsignedEvent);
+```
+
+### NIP-04 Encryption
+
+```javascript
+// Encrypt message
+const ciphertext = await signer.nip04Encrypt(
+ recipientPubkey,
+ 'Secret message'
+);
+
+// Decrypt message
+const plaintext = await signer.nip04Decrypt(
+ senderPubkey,
+ ciphertext
+);
+```
+
+### NIP-44 Encryption
+
+```javascript
+// Encrypt with NIP-44 (preferred)
+const ciphertext = await signer.nip44Encrypt(
+ recipientPubkey,
+ 'Secret message'
+);
+
+// Decrypt
+const plaintext = await signer.nip44Decrypt(
+ senderPubkey,
+ ciphertext
+);
+```
+
+## NIP-07 Signer
+
+### Browser Extension Integration
+
+```javascript
+import { Nip07Signer } from 'applesauce-signers';
+
+// Check if extension is available
+if (window.nostr) {
+ const signer = new Nip07Signer();
+
+ // Get public key (may prompt user)
+ const pubkey = await signer.getPublicKey();
+
+ // Sign event (prompts user)
+ const signedEvent = await signer.signEvent(unsignedEvent);
+}
+```
+
+### Handling Extension Availability
+
+```javascript
+function getAvailableSigner() {
+ if (typeof window !== 'undefined' && window.nostr) {
+ return new Nip07Signer();
+ }
+ return null;
+}
+
+// Wait for extension to load
+async function waitForExtension(timeout = 3000) {
+ const start = Date.now();
+
+ while (Date.now() - start < timeout) {
+ if (window.nostr) {
+ return new Nip07Signer();
+ }
+ await new Promise(r => setTimeout(r, 100));
+ }
+
+ return null;
+}
+```
+
+### Extension Permissions
+
+```javascript
+// Some extensions support granular permissions
+const signer = new Nip07Signer();
+
+// Request specific permissions
+try {
+ // This varies by extension
+ await window.nostr.enable();
+} catch (error) {
+ console.log('User denied permission');
+}
+```
+
+## NIP-46 Remote Signer
+
+### Nostr Connect
+
+```javascript
+import { Nip46Signer } from 'applesauce-signers';
+
+// Create remote signer
+const signer = new Nip46Signer({
+ // Remote signer's pubkey
+ remotePubkey: signerPubkey,
+
+ // Relays for communication
+ relays: ['wss://relay.example.com'],
+
+ // Local secret key for encryption
+ localSecretKey: localSecretKey,
+
+ // Optional: custom client name
+ clientName: 'My Nostr App'
+});
+
+// Connect to remote signer
+await signer.connect();
+
+// Get public key
+const pubkey = await signer.getPublicKey();
+
+// Sign event
+const signedEvent = await signer.signEvent(unsignedEvent);
+
+// Disconnect when done
+signer.disconnect();
+```
+
+### Connection URL
+
+```javascript
+// Parse nostrconnect:// URL
+function parseNostrConnectUrl(url) {
+ const parsed = new URL(url);
+
+ return {
+ pubkey: parsed.pathname.replace('//', ''),
+ relay: parsed.searchParams.get('relay'),
+ secret: parsed.searchParams.get('secret')
+ };
+}
+
+// Create signer from URL
+const { pubkey, relay, secret } = parseNostrConnectUrl(connectUrl);
+
+const signer = new Nip46Signer({
+ remotePubkey: pubkey,
+ relays: [relay],
+ localSecretKey: generateSecretKey(),
+ secret: secret
+});
+```
+
+### Bunker URL
+
+```javascript
+// Parse bunker:// URL (NIP-46)
+function parseBunkerUrl(url) {
+ const parsed = new URL(url);
+
+ return {
+ pubkey: parsed.pathname.replace('//', ''),
+ relays: parsed.searchParams.getAll('relay'),
+ secret: parsed.searchParams.get('secret')
+ };
+}
+
+const { pubkey, relays, secret } = parseBunkerUrl(bunkerUrl);
+```
+
+## Signer Management
+
+### Signer Store
+
+```javascript
+import { SignerStore } from 'applesauce-signers';
+
+const signerStore = new SignerStore();
+
+// Set active signer
+signerStore.setSigner(signer);
+
+// Get active signer
+const activeSigner = signerStore.getSigner();
+
+// Clear signer (logout)
+signerStore.clearSigner();
+
+// Observable for signer changes
+signerStore.signer$.subscribe(signer => {
+ if (signer) {
+ console.log('Logged in');
+ } else {
+ console.log('Logged out');
+ }
+});
+```
+
+### Multi-Account Support
+
+```javascript
+class AccountManager {
+ constructor() {
+ this.accounts = new Map();
+ this.activeAccount = null;
+ }
+
+ addAccount(pubkey, signer) {
+ this.accounts.set(pubkey, signer);
+ }
+
+ removeAccount(pubkey) {
+ this.accounts.delete(pubkey);
+ if (this.activeAccount === pubkey) {
+ this.activeAccount = null;
+ }
+ }
+
+ switchAccount(pubkey) {
+ if (this.accounts.has(pubkey)) {
+ this.activeAccount = pubkey;
+ return this.accounts.get(pubkey);
+ }
+ return null;
+ }
+
+ getActiveSigner() {
+ return this.activeAccount
+ ? this.accounts.get(this.activeAccount)
+ : null;
+ }
+}
+```
+
+## Custom Signers
+
+### Implementing a Custom Signer
+
+```javascript
+class CustomSigner {
+ constructor(options) {
+ this.options = options;
+ }
+
+ async getPublicKey() {
+ // Return public key
+ return this.options.pubkey;
+ }
+
+ async signEvent(event) {
+ // Implement signing logic
+ // Could call external API, hardware wallet, etc.
+
+ const signedEvent = await this.externalSign(event);
+ return signedEvent;
+ }
+
+ async nip04Encrypt(pubkey, plaintext) {
+ // Implement NIP-04 encryption
+ throw new Error('NIP-04 not supported');
+ }
+
+ async nip04Decrypt(pubkey, ciphertext) {
+ throw new Error('NIP-04 not supported');
+ }
+
+ async nip44Encrypt(pubkey, plaintext) {
+ // Implement NIP-44 encryption
+ throw new Error('NIP-44 not supported');
+ }
+
+ async nip44Decrypt(pubkey, ciphertext) {
+ throw new Error('NIP-44 not supported');
+ }
+}
+```
+
+### Hardware Wallet Signer
+
+```javascript
+class HardwareWalletSigner {
+ constructor(devicePath) {
+ this.devicePath = devicePath;
+ }
+
+ async connect() {
+ // Connect to hardware device
+ this.device = await connectToDevice(this.devicePath);
+ }
+
+ async getPublicKey() {
+ // Get public key from device
+ return await this.device.getNostrPubkey();
+ }
+
+ async signEvent(event) {
+ // Sign on device (user confirms on device)
+ const signature = await this.device.signNostrEvent(event);
+
+ return {
+ ...event,
+ pubkey: await this.getPublicKey(),
+ id: getEventHash(event),
+ sig: signature
+ };
+ }
+}
+```
+
+### Read-Only Signer
+
+```javascript
+class ReadOnlySigner {
+ constructor(pubkey) {
+ this.pubkey = pubkey;
+ }
+
+ async getPublicKey() {
+ return this.pubkey;
+ }
+
+ async signEvent(event) {
+ throw new Error('Read-only mode: cannot sign events');
+ }
+
+ async nip04Encrypt(pubkey, plaintext) {
+ throw new Error('Read-only mode: cannot encrypt');
+ }
+
+ async nip04Decrypt(pubkey, ciphertext) {
+ throw new Error('Read-only mode: cannot decrypt');
+ }
+}
+```
+
+## Signing Utilities
+
+### Event Creation Helper
+
+```javascript
+async function createAndSignEvent(signer, template) {
+ const pubkey = await signer.getPublicKey();
+
+ const event = {
+ ...template,
+ pubkey,
+ created_at: template.created_at || Math.floor(Date.now() / 1000)
+ };
+
+ return await signer.signEvent(event);
+}
+
+// Usage
+const signedNote = await createAndSignEvent(signer, {
+ kind: 1,
+ content: 'Hello!',
+ tags: []
+});
+```
+
+### Batch Signing
+
+```javascript
+async function signEvents(signer, events) {
+ const signed = [];
+
+ for (const event of events) {
+ const signedEvent = await signer.signEvent(event);
+ signed.push(signedEvent);
+ }
+
+ return signed;
+}
+
+// With parallelization (if signer supports)
+async function signEventsParallel(signer, events) {
+ return Promise.all(
+ events.map(event => signer.signEvent(event))
+ );
+}
+```
+
+## Svelte Integration
+
+### Signer Context
+
+```svelte
+
+
+
+
+```
+
+```svelte
+
+
+```
+
+### Login Component
+
+```svelte
+
+
+{#if $signer}
+ Logout
+{:else}
+
+ Login with Extension
+
+
+
+
+
+ Login with Key
+
+
+{/if}
+```
+
+## Best Practices
+
+### Security
+
+1. **Never store secret keys in plain text** - Use secure storage
+2. **Prefer NIP-07** - Let extensions manage keys
+3. **Clear keys on logout** - Don't leave in memory
+4. **Validate before signing** - Check event content
+
+### User Experience
+
+1. **Show signing status** - Loading states
+2. **Handle rejections gracefully** - User may cancel
+3. **Provide fallbacks** - Multiple login options
+4. **Remember preferences** - Store signer type
+
+### Error Handling
+
+```javascript
+async function safeSign(signer, event) {
+ try {
+ return await signer.signEvent(event);
+ } catch (error) {
+ if (error.message.includes('rejected')) {
+ console.log('User rejected signing');
+ return null;
+ }
+ if (error.message.includes('timeout')) {
+ console.log('Signing timed out');
+ return null;
+ }
+ throw error;
+ }
+}
+```
+
+### Permission Checking
+
+```javascript
+function hasEncryptionSupport(signer) {
+ return typeof signer.nip04Encrypt === 'function' ||
+ typeof signer.nip44Encrypt === 'function';
+}
+
+function getEncryptionMethod(signer) {
+ // Prefer NIP-44
+ if (typeof signer.nip44Encrypt === 'function') {
+ return 'nip44';
+ }
+ if (typeof signer.nip04Encrypt === 'function') {
+ return 'nip04';
+ }
+ return null;
+}
+```
+
+## Common Patterns
+
+### Signer Detection
+
+```javascript
+async function detectSigners() {
+ const available = [];
+
+ // Check NIP-07
+ if (typeof window !== 'undefined' && window.nostr) {
+ available.push({
+ type: 'nip07',
+ name: 'Browser Extension',
+ create: () => new Nip07Signer()
+ });
+ }
+
+ // Check stored credentials
+ const storedKey = localStorage.getItem('nsec');
+ if (storedKey) {
+ available.push({
+ type: 'stored',
+ name: 'Saved Key',
+ create: () => new SimpleSigner(storedKey)
+ });
+ }
+
+ return available;
+}
+```
+
+### Auto-Reconnect for NIP-46
+
+```javascript
+class ReconnectingNip46Signer {
+ constructor(options) {
+ this.options = options;
+ this.signer = null;
+ }
+
+ async connect() {
+ this.signer = new Nip46Signer(this.options);
+ await this.signer.connect();
+ }
+
+ async signEvent(event) {
+ try {
+ return await this.signer.signEvent(event);
+ } catch (error) {
+ if (error.message.includes('disconnected')) {
+ await this.connect();
+ return await this.signer.signEvent(event);
+ }
+ throw error;
+ }
+ }
+}
+```
+
+### Signer Type Persistence
+
+```javascript
+const SIGNER_KEY = 'nostr_signer_type';
+
+function saveSigner(type, data) {
+ localStorage.setItem(SIGNER_KEY, JSON.stringify({ type, data }));
+}
+
+async function restoreSigner() {
+ const saved = localStorage.getItem(SIGNER_KEY);
+ if (!saved) return null;
+
+ const { type, data } = JSON.parse(saved);
+
+ switch (type) {
+ case 'nip07':
+ if (window.nostr) {
+ return new Nip07Signer();
+ }
+ break;
+ case 'simple':
+ // Don't store secret keys!
+ break;
+ case 'nip46':
+ const signer = new Nip46Signer(data);
+ await signer.connect();
+ return signer;
+ }
+
+ return null;
+}
+```
+
+## Troubleshooting
+
+### Common Issues
+
+**Extension not detected:**
+- Wait for page load
+- Check window.nostr exists
+- Verify extension is enabled
+
+**Signing rejected:**
+- User cancelled in extension
+- Handle gracefully with error message
+
+**NIP-46 connection fails:**
+- Check relay is accessible
+- Verify remote signer is online
+- Check secret matches
+
+**Encryption not supported:**
+- Check signer has encrypt methods
+- Fall back to alternative method
+- Show user appropriate error
+
+## References
+
+- **applesauce GitHub**: https://github.com/hzrd149/applesauce
+- **NIP-07 Specification**: https://github.com/nostr-protocol/nips/blob/master/07.md
+- **NIP-46 Specification**: https://github.com/nostr-protocol/nips/blob/master/46.md
+- **nostr-tools**: https://github.com/nbd-wtf/nostr-tools
+
+## Related Skills
+
+- **nostr-tools** - Event creation and signing utilities
+- **applesauce-core** - Event stores and queries
+- **nostr** - Nostr protocol fundamentals
+- **svelte** - Building Nostr UIs
diff --git a/.claude/skills/nostr-tools/SKILL.md b/.claude/skills/nostr-tools/SKILL.md
new file mode 100644
index 0000000..905d41c
--- /dev/null
+++ b/.claude/skills/nostr-tools/SKILL.md
@@ -0,0 +1,767 @@
+---
+name: nostr-tools
+description: This skill should be used when working with nostr-tools library for Nostr protocol operations, including event creation, signing, filtering, relay communication, and NIP implementations. Provides comprehensive knowledge of nostr-tools APIs and patterns.
+---
+
+# nostr-tools Skill
+
+This skill provides comprehensive knowledge and patterns for working with nostr-tools, the most popular JavaScript/TypeScript library for Nostr protocol development.
+
+## When to Use This Skill
+
+Use this skill when:
+- Building Nostr clients or applications
+- Creating and signing Nostr events
+- Connecting to Nostr relays
+- Implementing NIP features
+- Working with Nostr keys and cryptography
+- Filtering and querying events
+- Building relay pools or connections
+- Implementing NIP-44/NIP-04 encryption
+
+## Core Concepts
+
+### nostr-tools Overview
+
+nostr-tools provides:
+- **Event handling** - Create, sign, verify events
+- **Key management** - Generate, convert, encode keys
+- **Relay communication** - Connect, subscribe, publish
+- **NIP implementations** - NIP-04, NIP-05, NIP-19, NIP-44, etc.
+- **Cryptographic operations** - Schnorr signatures, encryption
+- **Filter building** - Query events by various criteria
+
+### Installation
+
+```bash
+npm install nostr-tools
+```
+
+### Basic Imports
+
+```javascript
+// Core functionality
+import {
+ SimplePool,
+ generateSecretKey,
+ getPublicKey,
+ finalizeEvent,
+ verifyEvent
+} from 'nostr-tools';
+
+// NIP-specific imports
+import { nip04, nip05, nip19, nip44 } from 'nostr-tools';
+
+// Relay operations
+import { Relay } from 'nostr-tools/relay';
+```
+
+## Key Management
+
+### Generating Keys
+
+```javascript
+import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
+
+// Generate new secret key (Uint8Array)
+const secretKey = generateSecretKey();
+
+// Derive public key
+const publicKey = getPublicKey(secretKey);
+
+console.log('Secret key:', bytesToHex(secretKey));
+console.log('Public key:', publicKey); // hex string
+```
+
+### Key Encoding (NIP-19)
+
+```javascript
+import { nip19 } from 'nostr-tools';
+
+// Encode to bech32
+const nsec = nip19.nsecEncode(secretKey);
+const npub = nip19.npubEncode(publicKey);
+const note = nip19.noteEncode(eventId);
+
+console.log(nsec); // nsec1...
+console.log(npub); // npub1...
+console.log(note); // note1...
+
+// Decode from bech32
+const { type, data } = nip19.decode(npub);
+// type: 'npub', data: publicKey (hex)
+
+// Encode profile reference (nprofile)
+const nprofile = nip19.nprofileEncode({
+ pubkey: publicKey,
+ relays: ['wss://relay.example.com']
+});
+
+// Encode event reference (nevent)
+const nevent = nip19.neventEncode({
+ id: eventId,
+ relays: ['wss://relay.example.com'],
+ author: publicKey,
+ kind: 1
+});
+
+// Encode address (naddr) for replaceable events
+const naddr = nip19.naddrEncode({
+ identifier: 'my-article',
+ pubkey: publicKey,
+ kind: 30023,
+ relays: ['wss://relay.example.com']
+});
+```
+
+## Event Operations
+
+### Event Structure
+
+```javascript
+// Unsigned event template
+const eventTemplate = {
+ kind: 1,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [],
+ content: 'Hello Nostr!'
+};
+
+// Signed event (after finalizeEvent)
+const signedEvent = {
+ id: '...', // 32-byte sha256 hash as hex
+ pubkey: '...', // 32-byte public key as hex
+ created_at: 1234567890,
+ kind: 1,
+ tags: [],
+ content: 'Hello Nostr!',
+ sig: '...' // 64-byte Schnorr signature as hex
+};
+```
+
+### Creating and Signing Events
+
+```javascript
+import { finalizeEvent, verifyEvent } from 'nostr-tools/pure';
+
+// Create event template
+const eventTemplate = {
+ kind: 1,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [
+ ['p', publicKey], // Mention
+ ['e', eventId, '', 'reply'], // Reply
+ ['t', 'nostr'] // Hashtag
+ ],
+ content: 'Hello Nostr!'
+};
+
+// Sign event
+const signedEvent = finalizeEvent(eventTemplate, secretKey);
+
+// Verify event
+const isValid = verifyEvent(signedEvent);
+console.log('Event valid:', isValid);
+```
+
+### Event Kinds
+
+```javascript
+// Common event kinds
+const KINDS = {
+ Metadata: 0, // Profile metadata (NIP-01)
+ Text: 1, // Short text note (NIP-01)
+ RecommendRelay: 2, // Relay recommendation
+ Contacts: 3, // Contact list (NIP-02)
+ EncryptedDM: 4, // Encrypted DM (NIP-04)
+ EventDeletion: 5, // Delete events (NIP-09)
+ Repost: 6, // Repost (NIP-18)
+ Reaction: 7, // Reaction (NIP-25)
+ ChannelCreation: 40, // Channel (NIP-28)
+ ChannelMessage: 42, // Channel message
+ Zap: 9735, // Zap receipt (NIP-57)
+ Report: 1984, // Report (NIP-56)
+ RelayList: 10002, // Relay list (NIP-65)
+ Article: 30023, // Long-form content (NIP-23)
+};
+```
+
+### Creating Specific Events
+
+```javascript
+// Profile metadata (kind 0)
+const profileEvent = finalizeEvent({
+ kind: 0,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [],
+ content: JSON.stringify({
+ name: 'Alice',
+ about: 'Nostr enthusiast',
+ picture: 'https://example.com/avatar.jpg',
+ nip05: 'alice@example.com',
+ lud16: 'alice@getalby.com'
+ })
+}, secretKey);
+
+// Contact list (kind 3)
+const contactsEvent = finalizeEvent({
+ kind: 3,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [
+ ['p', pubkey1, 'wss://relay1.com', 'alice'],
+ ['p', pubkey2, 'wss://relay2.com', 'bob'],
+ ['p', pubkey3, '', 'carol']
+ ],
+ content: '' // Or JSON relay preferences
+}, secretKey);
+
+// Reply to an event
+const replyEvent = finalizeEvent({
+ kind: 1,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [
+ ['e', rootEventId, '', 'root'],
+ ['e', parentEventId, '', 'reply'],
+ ['p', parentEventPubkey]
+ ],
+ content: 'This is a reply'
+}, secretKey);
+
+// Reaction (kind 7)
+const reactionEvent = finalizeEvent({
+ kind: 7,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [
+ ['e', eventId],
+ ['p', eventPubkey]
+ ],
+ content: '+' // or '-' or emoji
+}, secretKey);
+
+// Delete event (kind 5)
+const deleteEvent = finalizeEvent({
+ kind: 5,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [
+ ['e', eventIdToDelete],
+ ['e', anotherEventIdToDelete]
+ ],
+ content: 'Deletion reason'
+}, secretKey);
+```
+
+## Relay Communication
+
+### Using SimplePool
+
+SimplePool is the recommended way to interact with multiple relays:
+
+```javascript
+import { SimplePool } from 'nostr-tools/pool';
+
+const pool = new SimplePool();
+const relays = [
+ 'wss://relay.damus.io',
+ 'wss://nos.lol',
+ 'wss://relay.nostr.band'
+];
+
+// Subscribe to events
+const subscription = pool.subscribeMany(
+ relays,
+ [
+ {
+ kinds: [1],
+ authors: [publicKey],
+ limit: 10
+ }
+ ],
+ {
+ onevent(event) {
+ console.log('Received event:', event);
+ },
+ oneose() {
+ console.log('End of stored events');
+ }
+ }
+);
+
+// Close subscription when done
+subscription.close();
+
+// Publish event to all relays
+const results = await Promise.allSettled(
+ pool.publish(relays, signedEvent)
+);
+
+// Query events (returns Promise)
+const events = await pool.querySync(relays, {
+ kinds: [0],
+ authors: [publicKey]
+});
+
+// Get single event
+const event = await pool.get(relays, {
+ ids: [eventId]
+});
+
+// Close pool when done
+pool.close(relays);
+```
+
+### Direct Relay Connection
+
+```javascript
+import { Relay } from 'nostr-tools/relay';
+
+const relay = await Relay.connect('wss://relay.damus.io');
+
+console.log(`Connected to ${relay.url}`);
+
+// Subscribe
+const sub = relay.subscribe([
+ {
+ kinds: [1],
+ limit: 100
+ }
+], {
+ onevent(event) {
+ console.log('Event:', event);
+ },
+ oneose() {
+ console.log('EOSE');
+ sub.close();
+ }
+});
+
+// Publish
+await relay.publish(signedEvent);
+
+// Close
+relay.close();
+```
+
+### Handling Connection States
+
+```javascript
+import { Relay } from 'nostr-tools/relay';
+
+const relay = await Relay.connect('wss://relay.example.com');
+
+// Listen for disconnect
+relay.onclose = () => {
+ console.log('Relay disconnected');
+};
+
+// Check connection status
+console.log('Connected:', relay.connected);
+```
+
+## Filters
+
+### Filter Structure
+
+```javascript
+const filter = {
+ // Event IDs
+ ids: ['abc123...'],
+
+ // Authors (pubkeys)
+ authors: ['pubkey1', 'pubkey2'],
+
+ // Event kinds
+ kinds: [1, 6, 7],
+
+ // Tags (single-letter keys)
+ '#e': ['eventId1', 'eventId2'],
+ '#p': ['pubkey1'],
+ '#t': ['nostr', 'bitcoin'],
+ '#d': ['article-identifier'],
+
+ // Time range
+ since: 1704067200, // Unix timestamp
+ until: 1704153600,
+
+ // Limit results
+ limit: 100,
+
+ // Search (NIP-50, if relay supports)
+ search: 'nostr protocol'
+};
+```
+
+### Common Filter Patterns
+
+```javascript
+// User's recent posts
+const userPosts = {
+ kinds: [1],
+ authors: [userPubkey],
+ limit: 50
+};
+
+// User's profile
+const userProfile = {
+ kinds: [0],
+ authors: [userPubkey]
+};
+
+// User's contacts
+const userContacts = {
+ kinds: [3],
+ authors: [userPubkey]
+};
+
+// Replies to an event
+const replies = {
+ kinds: [1],
+ '#e': [eventId]
+};
+
+// Reactions to an event
+const reactions = {
+ kinds: [7],
+ '#e': [eventId]
+};
+
+// Feed from followed users
+const feed = {
+ kinds: [1, 6],
+ authors: followedPubkeys,
+ limit: 100
+};
+
+// Events mentioning user
+const mentions = {
+ kinds: [1],
+ '#p': [userPubkey],
+ limit: 50
+};
+
+// Hashtag search
+const hashtagEvents = {
+ kinds: [1],
+ '#t': ['bitcoin'],
+ limit: 100
+};
+
+// Replaceable event by d-tag
+const replaceableEvent = {
+ kinds: [30023],
+ authors: [authorPubkey],
+ '#d': ['article-slug']
+};
+```
+
+### Multiple Filters
+
+```javascript
+// Subscribe with multiple filters (OR logic)
+const filters = [
+ { kinds: [1], authors: [userPubkey], limit: 20 },
+ { kinds: [1], '#p': [userPubkey], limit: 20 }
+];
+
+pool.subscribeMany(relays, filters, {
+ onevent(event) {
+ // Receives events matching ANY filter
+ }
+});
+```
+
+## Encryption
+
+### NIP-04 (Legacy DMs)
+
+```javascript
+import { nip04 } from 'nostr-tools';
+
+// Encrypt message
+const ciphertext = await nip04.encrypt(
+ secretKey,
+ recipientPubkey,
+ 'Hello, this is secret!'
+);
+
+// Create encrypted DM event
+const dmEvent = finalizeEvent({
+ kind: 4,
+ created_at: Math.floor(Date.now() / 1000),
+ tags: [['p', recipientPubkey]],
+ content: ciphertext
+}, secretKey);
+
+// Decrypt message
+const plaintext = await nip04.decrypt(
+ secretKey,
+ senderPubkey,
+ ciphertext
+);
+```
+
+### NIP-44 (Modern Encryption)
+
+```javascript
+import { nip44 } from 'nostr-tools';
+
+// Get conversation key (cache this for multiple messages)
+const conversationKey = nip44.getConversationKey(
+ secretKey,
+ recipientPubkey
+);
+
+// Encrypt
+const ciphertext = nip44.encrypt(
+ 'Hello with NIP-44!',
+ conversationKey
+);
+
+// Decrypt
+const plaintext = nip44.decrypt(
+ ciphertext,
+ conversationKey
+);
+```
+
+## NIP Implementations
+
+### NIP-05 (DNS Identifier)
+
+```javascript
+import { nip05 } from 'nostr-tools';
+
+// Query NIP-05 identifier
+const profile = await nip05.queryProfile('alice@example.com');
+
+if (profile) {
+ console.log('Pubkey:', profile.pubkey);
+ console.log('Relays:', profile.relays);
+}
+
+// Verify NIP-05 for a pubkey
+const isValid = await nip05.queryProfile('alice@example.com')
+ .then(p => p?.pubkey === expectedPubkey);
+```
+
+### NIP-10 (Reply Threading)
+
+```javascript
+import { nip10 } from 'nostr-tools';
+
+// Parse reply tags
+const parsed = nip10.parse(event);
+
+console.log('Root:', parsed.root); // Original event
+console.log('Reply:', parsed.reply); // Direct parent
+console.log('Mentions:', parsed.mentions); // Other mentions
+console.log('Profiles:', parsed.profiles); // Mentioned pubkeys
+```
+
+### NIP-21 (nostr: URIs)
+
+```javascript
+// Parse nostr: URIs
+const uri = 'nostr:npub1...';
+const { type, data } = nip19.decode(uri.replace('nostr:', ''));
+```
+
+### NIP-27 (Content References)
+
+```javascript
+// Parse nostr:npub and nostr:note references in content
+const content = 'Check out nostr:npub1abc... and nostr:note1xyz...';
+
+const references = content.match(/nostr:(n[a-z]+1[a-z0-9]+)/g);
+references?.forEach(ref => {
+ const decoded = nip19.decode(ref.replace('nostr:', ''));
+ console.log(decoded.type, decoded.data);
+});
+```
+
+### NIP-57 (Zaps)
+
+```javascript
+import { nip57 } from 'nostr-tools';
+
+// Validate zap receipt
+const zapReceipt = await pool.get(relays, {
+ kinds: [9735],
+ '#e': [eventId]
+});
+
+const validatedZap = await nip57.validateZapRequest(zapReceipt);
+```
+
+## Utilities
+
+### Hex and Bytes Conversion
+
+```javascript
+import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
+
+// Convert secret key to hex
+const secretKeyHex = bytesToHex(secretKey);
+
+// Convert hex back to bytes
+const secretKeyBytes = hexToBytes(secretKeyHex);
+```
+
+### Event ID Calculation
+
+```javascript
+import { getEventHash } from 'nostr-tools/pure';
+
+// Calculate event ID without signing
+const eventId = getEventHash(unsignedEvent);
+```
+
+### Signature Operations
+
+```javascript
+import {
+ getSignature,
+ verifyEvent
+} from 'nostr-tools/pure';
+
+// Sign event data
+const signature = getSignature(unsignedEvent, secretKey);
+
+// Verify complete event
+const isValid = verifyEvent(signedEvent);
+```
+
+## Best Practices
+
+### Connection Management
+
+1. **Use SimplePool** - Manages connections efficiently
+2. **Limit concurrent connections** - Don't connect to too many relays
+3. **Handle disconnections** - Implement reconnection logic
+4. **Close subscriptions** - Always close when done
+
+### Event Handling
+
+1. **Verify events** - Always verify signatures
+2. **Deduplicate** - Events may come from multiple relays
+3. **Handle replaceable events** - Latest by created_at wins
+4. **Validate content** - Don't trust event content blindly
+
+### Key Security
+
+1. **Never expose secret keys** - Keep in secure storage
+2. **Use NIP-07 in browsers** - Let extensions handle signing
+3. **Validate input** - Check key formats before use
+
+### Performance
+
+1. **Cache events** - Avoid re-fetching
+2. **Use filters wisely** - Be specific, use limits
+3. **Batch operations** - Combine related queries
+4. **Close idle connections** - Free up resources
+
+## Common Patterns
+
+### Building a Feed
+
+```javascript
+const pool = new SimplePool();
+const relays = ['wss://relay.damus.io', 'wss://nos.lol'];
+
+async function loadFeed(followedPubkeys) {
+ const events = await pool.querySync(relays, {
+ kinds: [1, 6],
+ authors: followedPubkeys,
+ limit: 100
+ });
+
+ // Sort by timestamp
+ return events.sort((a, b) => b.created_at - a.created_at);
+}
+```
+
+### Real-time Updates
+
+```javascript
+function subscribeToFeed(followedPubkeys, onEvent) {
+ return pool.subscribeMany(
+ relays,
+ [{ kinds: [1, 6], authors: followedPubkeys }],
+ {
+ onevent: onEvent,
+ oneose() {
+ console.log('Caught up with stored events');
+ }
+ }
+ );
+}
+```
+
+### Profile Loading
+
+```javascript
+async function loadProfile(pubkey) {
+ const [metadata] = await pool.querySync(relays, {
+ kinds: [0],
+ authors: [pubkey],
+ limit: 1
+ });
+
+ if (metadata) {
+ return JSON.parse(metadata.content);
+ }
+ return null;
+}
+```
+
+### Event Deduplication
+
+```javascript
+const seenEvents = new Set();
+
+function handleEvent(event) {
+ if (seenEvents.has(event.id)) {
+ return; // Skip duplicate
+ }
+ seenEvents.add(event.id);
+
+ // Process event...
+}
+```
+
+## Troubleshooting
+
+### Common Issues
+
+**Events not publishing:**
+- Check relay is writable
+- Verify event is properly signed
+- Check relay's accepted kinds
+
+**Subscription not receiving events:**
+- Verify filter syntax
+- Check relay has matching events
+- Ensure subscription isn't closed
+
+**Signature verification fails:**
+- Check event structure is correct
+- Verify keys are in correct format
+- Ensure event hasn't been modified
+
+**NIP-05 lookup fails:**
+- Check CORS headers on server
+- Verify .well-known path is correct
+- Handle network timeouts
+
+## References
+
+- **nostr-tools GitHub**: https://github.com/nbd-wtf/nostr-tools
+- **Nostr Protocol**: https://github.com/nostr-protocol/nostr
+- **NIPs Repository**: https://github.com/nostr-protocol/nips
+- **NIP-01 (Basic Protocol)**: https://github.com/nostr-protocol/nips/blob/master/01.md
+
+## Related Skills
+
+- **nostr** - Nostr protocol fundamentals
+- **svelte** - Building Nostr UIs with Svelte
+- **applesauce-core** - Higher-level Nostr client utilities
+- **applesauce-signers** - Nostr signing abstractions
diff --git a/.claude/skills/rollup/SKILL.md b/.claude/skills/rollup/SKILL.md
new file mode 100644
index 0000000..cd694cd
--- /dev/null
+++ b/.claude/skills/rollup/SKILL.md
@@ -0,0 +1,899 @@
+---
+name: rollup
+description: This skill should be used when working with Rollup module bundler, including configuration, plugins, code splitting, and build optimization. Provides comprehensive knowledge of Rollup patterns, plugin development, and bundling strategies.
+---
+
+# Rollup Skill
+
+This skill provides comprehensive knowledge and patterns for working with Rollup module bundler effectively.
+
+## When to Use This Skill
+
+Use this skill when:
+- Configuring Rollup for web applications
+- Setting up Rollup for library builds
+- Working with Rollup plugins
+- Implementing code splitting
+- Optimizing bundle size
+- Troubleshooting build issues
+- Integrating Rollup with Svelte or other frameworks
+- Developing custom Rollup plugins
+
+## Core Concepts
+
+### Rollup Overview
+
+Rollup is a module bundler that:
+- **Tree-shakes by default** - Removes unused code automatically
+- **ES module focused** - Native ESM output support
+- **Plugin-based** - Extensible architecture
+- **Multiple outputs** - Generate multiple formats from single input
+- **Code splitting** - Dynamic imports for lazy loading
+- **Scope hoisting** - Flattens modules for smaller bundles
+
+### Basic Configuration
+
+```javascript
+// rollup.config.js
+export default {
+ input: 'src/main.js',
+ output: {
+ file: 'dist/bundle.js',
+ format: 'esm'
+ }
+};
+```
+
+### Output Formats
+
+Rollup supports multiple output formats:
+
+| Format | Description | Use Case |
+|--------|-------------|----------|
+| `esm` | ES modules | Modern browsers, bundlers |
+| `cjs` | CommonJS | Node.js |
+| `iife` | Self-executing function | Script tags |
+| `umd` | Universal Module Definition | CDN, both environments |
+| `amd` | Asynchronous Module Definition | RequireJS |
+| `system` | SystemJS | SystemJS loader |
+
+## Configuration
+
+### Full Configuration Options
+
+```javascript
+// rollup.config.js
+import resolve from '@rollup/plugin-node-resolve';
+import commonjs from '@rollup/plugin-commonjs';
+import terser from '@rollup/plugin-terser';
+
+const production = !process.env.ROLLUP_WATCH;
+
+export default {
+ // Entry point(s)
+ input: 'src/main.js',
+
+ // Output configuration
+ output: {
+ // Output file or directory
+ file: 'dist/bundle.js',
+ // Or for code splitting:
+ // dir: 'dist',
+
+ // Output format
+ format: 'esm',
+
+ // Name for IIFE/UMD builds
+ name: 'MyBundle',
+
+ // Sourcemap generation
+ sourcemap: true,
+
+ // Global variables for external imports (IIFE/UMD)
+ globals: {
+ jquery: '$'
+ },
+
+ // Banner/footer comments
+ banner: '/* My library v1.0.0 */',
+ footer: '/* End of bundle */',
+
+ // Chunk naming for code splitting
+ chunkFileNames: '[name]-[hash].js',
+ entryFileNames: '[name].js',
+
+ // Manual chunks for code splitting
+ manualChunks: {
+ vendor: ['lodash', 'moment']
+ },
+
+ // Interop mode for default exports
+ interop: 'auto',
+
+ // Preserve modules structure
+ preserveModules: false,
+
+ // Exports mode
+ exports: 'auto' // 'default', 'named', 'none', 'auto'
+ },
+
+ // External dependencies (not bundled)
+ external: ['lodash', /^node:/],
+
+ // Plugin array
+ plugins: [
+ resolve({
+ browser: true,
+ dedupe: ['svelte']
+ }),
+ commonjs(),
+ production && terser()
+ ],
+
+ // Watch mode options
+ watch: {
+ include: 'src/**',
+ exclude: 'node_modules/**',
+ clearScreen: false
+ },
+
+ // Warning handling
+ onwarn(warning, warn) {
+ // Skip certain warnings
+ if (warning.code === 'CIRCULAR_DEPENDENCY') return;
+ warn(warning);
+ },
+
+ // Preserve entry signatures for code splitting
+ preserveEntrySignatures: 'strict',
+
+ // Treeshake options
+ treeshake: {
+ moduleSideEffects: false,
+ propertyReadSideEffects: false
+ }
+};
+```
+
+### Multiple Outputs
+
+```javascript
+export default {
+ input: 'src/main.js',
+ output: [
+ {
+ file: 'dist/bundle.esm.js',
+ format: 'esm'
+ },
+ {
+ file: 'dist/bundle.cjs.js',
+ format: 'cjs'
+ },
+ {
+ file: 'dist/bundle.umd.js',
+ format: 'umd',
+ name: 'MyLibrary'
+ }
+ ]
+};
+```
+
+### Multiple Entry Points
+
+```javascript
+export default {
+ input: {
+ main: 'src/main.js',
+ utils: 'src/utils.js'
+ },
+ output: {
+ dir: 'dist',
+ format: 'esm'
+ }
+};
+```
+
+### Array of Configurations
+
+```javascript
+export default [
+ {
+ input: 'src/main.js',
+ output: { file: 'dist/main.js', format: 'esm' }
+ },
+ {
+ input: 'src/worker.js',
+ output: { file: 'dist/worker.js', format: 'iife' }
+ }
+];
+```
+
+## Essential Plugins
+
+### @rollup/plugin-node-resolve
+
+Resolve node_modules imports:
+
+```javascript
+import resolve from '@rollup/plugin-node-resolve';
+
+export default {
+ plugins: [
+ resolve({
+ // Resolve browser field in package.json
+ browser: true,
+
+ // Prefer built-in modules
+ preferBuiltins: true,
+
+ // Only resolve these extensions
+ extensions: ['.mjs', '.js', '.json', '.node'],
+
+ // Dedupe packages (important for Svelte)
+ dedupe: ['svelte'],
+
+ // Main fields to check in package.json
+ mainFields: ['module', 'main', 'browser'],
+
+ // Export conditions
+ exportConditions: ['svelte', 'browser', 'module', 'import']
+ })
+ ]
+};
+```
+
+### @rollup/plugin-commonjs
+
+Convert CommonJS to ES modules:
+
+```javascript
+import commonjs from '@rollup/plugin-commonjs';
+
+export default {
+ plugins: [
+ commonjs({
+ // Include specific modules
+ include: /node_modules/,
+
+ // Exclude specific modules
+ exclude: ['node_modules/lodash-es/**'],
+
+ // Ignore conditional requires
+ ignoreDynamicRequires: false,
+
+ // Transform mixed ES/CJS modules
+ transformMixedEsModules: true,
+
+ // Named exports for specific modules
+ namedExports: {
+ 'react': ['createElement', 'Component']
+ }
+ })
+ ]
+};
+```
+
+### @rollup/plugin-terser
+
+Minify output:
+
+```javascript
+import terser from '@rollup/plugin-terser';
+
+export default {
+ plugins: [
+ terser({
+ compress: {
+ drop_console: true,
+ drop_debugger: true
+ },
+ mangle: true,
+ format: {
+ comments: false
+ }
+ })
+ ]
+};
+```
+
+### rollup-plugin-svelte
+
+Compile Svelte components:
+
+```javascript
+import svelte from 'rollup-plugin-svelte';
+import css from 'rollup-plugin-css-only';
+
+export default {
+ plugins: [
+ svelte({
+ // Enable dev mode
+ dev: !production,
+
+ // Emit CSS as a separate file
+ emitCss: true,
+
+ // Preprocess (SCSS, TypeScript, etc.)
+ preprocess: sveltePreprocess(),
+
+ // Compiler options
+ compilerOptions: {
+ dev: !production
+ },
+
+ // Custom element mode
+ customElement: false
+ }),
+
+ // Extract CSS to separate file
+ css({ output: 'bundle.css' })
+ ]
+};
+```
+
+### Other Common Plugins
+
+```javascript
+import json from '@rollup/plugin-json';
+import replace from '@rollup/plugin-replace';
+import alias from '@rollup/plugin-alias';
+import image from '@rollup/plugin-image';
+import copy from 'rollup-plugin-copy';
+import livereload from 'rollup-plugin-livereload';
+
+export default {
+ plugins: [
+ // Import JSON files
+ json(),
+
+ // Replace strings in code
+ replace({
+ preventAssignment: true,
+ 'process.env.NODE_ENV': JSON.stringify('production'),
+ '__VERSION__': JSON.stringify('1.0.0')
+ }),
+
+ // Path aliases
+ alias({
+ entries: [
+ { find: '@', replacement: './src' },
+ { find: 'utils', replacement: './src/utils' }
+ ]
+ }),
+
+ // Import images
+ image(),
+
+ // Copy static files
+ copy({
+ targets: [
+ { src: 'public/*', dest: 'dist' }
+ ]
+ }),
+
+ // Live reload in dev
+ !production && livereload('dist')
+ ]
+};
+```
+
+## Code Splitting
+
+### Dynamic Imports
+
+```javascript
+// Automatically creates chunks
+async function loadFeature() {
+ const { feature } = await import('./feature.js');
+ feature();
+}
+```
+
+Configuration for code splitting:
+
+```javascript
+export default {
+ input: 'src/main.js',
+ output: {
+ dir: 'dist',
+ format: 'esm',
+ chunkFileNames: 'chunks/[name]-[hash].js'
+ }
+};
+```
+
+### Manual Chunks
+
+```javascript
+export default {
+ output: {
+ manualChunks: {
+ // Vendor chunk
+ vendor: ['lodash', 'moment'],
+
+ // Or use a function for more control
+ manualChunks(id) {
+ if (id.includes('node_modules')) {
+ return 'vendor';
+ }
+ }
+ }
+ }
+};
+```
+
+### Advanced Chunking Strategy
+
+```javascript
+export default {
+ output: {
+ manualChunks(id, { getModuleInfo }) {
+ // Separate chunks by feature
+ if (id.includes('/features/auth/')) {
+ return 'auth';
+ }
+ if (id.includes('/features/dashboard/')) {
+ return 'dashboard';
+ }
+
+ // Vendor chunks by package
+ if (id.includes('node_modules')) {
+ const match = id.match(/node_modules\/([^/]+)/);
+ if (match) {
+ const packageName = match[1];
+ // Group small packages
+ const smallPackages = ['lodash', 'date-fns'];
+ if (smallPackages.includes(packageName)) {
+ return 'vendor-utils';
+ }
+ return `vendor-${packageName}`;
+ }
+ }
+ }
+ }
+};
+```
+
+## Watch Mode
+
+### Configuration
+
+```javascript
+export default {
+ watch: {
+ // Files to watch
+ include: 'src/**',
+
+ // Files to ignore
+ exclude: 'node_modules/**',
+
+ // Don't clear screen on rebuild
+ clearScreen: false,
+
+ // Rebuild delay
+ buildDelay: 0,
+
+ // Watch chokidar options
+ chokidar: {
+ usePolling: true
+ }
+ }
+};
+```
+
+### CLI Watch Mode
+
+```bash
+# Watch mode
+rollup -c -w
+
+# With environment variable
+ROLLUP_WATCH=true rollup -c
+```
+
+## Plugin Development
+
+### Plugin Structure
+
+```javascript
+function myPlugin(options = {}) {
+ return {
+ // Plugin name (required)
+ name: 'my-plugin',
+
+ // Build hooks
+ options(inputOptions) {
+ // Modify input options
+ return inputOptions;
+ },
+
+ buildStart(inputOptions) {
+ // Called on build start
+ },
+
+ resolveId(source, importer, options) {
+ // Custom module resolution
+ if (source === 'virtual-module') {
+ return source;
+ }
+ return null; // Defer to other plugins
+ },
+
+ load(id) {
+ // Load module content
+ if (id === 'virtual-module') {
+ return 'export default "Hello"';
+ }
+ return null;
+ },
+
+ transform(code, id) {
+ // Transform module code
+ if (id.endsWith('.txt')) {
+ return {
+ code: `export default ${JSON.stringify(code)}`,
+ map: null
+ };
+ }
+ },
+
+ buildEnd(error) {
+ // Called when build ends
+ if (error) {
+ console.error('Build failed:', error);
+ }
+ },
+
+ // Output generation hooks
+ renderStart(outputOptions, inputOptions) {
+ // Called before output generation
+ },
+
+ banner() {
+ return '/* Custom banner */';
+ },
+
+ footer() {
+ return '/* Custom footer */';
+ },
+
+ renderChunk(code, chunk, options) {
+ // Transform output chunk
+ return code;
+ },
+
+ generateBundle(options, bundle) {
+ // Modify output bundle
+ for (const fileName in bundle) {
+ const chunk = bundle[fileName];
+ if (chunk.type === 'chunk') {
+ // Modify chunk
+ }
+ }
+ },
+
+ writeBundle(options, bundle) {
+ // After bundle is written
+ },
+
+ closeBundle() {
+ // Called when bundle is closed
+ }
+ };
+}
+
+export default myPlugin;
+```
+
+### Plugin with Rollup Utils
+
+```javascript
+import { createFilter } from '@rollup/pluginutils';
+
+function myTransformPlugin(options = {}) {
+ const filter = createFilter(options.include, options.exclude);
+
+ return {
+ name: 'my-transform',
+
+ transform(code, id) {
+ if (!filter(id)) return null;
+
+ // Transform code
+ const transformed = code.replace(/foo/g, 'bar');
+
+ return {
+ code: transformed,
+ map: null // Or generate sourcemap
+ };
+ }
+ };
+}
+```
+
+## Svelte Integration
+
+### Complete Svelte Setup
+
+```javascript
+// rollup.config.js
+import svelte from 'rollup-plugin-svelte';
+import commonjs from '@rollup/plugin-commonjs';
+import resolve from '@rollup/plugin-node-resolve';
+import terser from '@rollup/plugin-terser';
+import css from 'rollup-plugin-css-only';
+import livereload from 'rollup-plugin-livereload';
+
+const production = !process.env.ROLLUP_WATCH;
+
+function serve() {
+ let server;
+
+ function toExit() {
+ if (server) server.kill(0);
+ }
+
+ return {
+ writeBundle() {
+ if (server) return;
+ server = require('child_process').spawn(
+ 'npm',
+ ['run', 'start', '--', '--dev'],
+ {
+ stdio: ['ignore', 'inherit', 'inherit'],
+ shell: true
+ }
+ );
+
+ process.on('SIGTERM', toExit);
+ process.on('exit', toExit);
+ }
+ };
+}
+
+export default {
+ input: 'src/main.js',
+ output: {
+ sourcemap: true,
+ format: 'iife',
+ name: 'app',
+ file: 'public/build/bundle.js'
+ },
+ plugins: [
+ svelte({
+ compilerOptions: {
+ dev: !production
+ }
+ }),
+ css({ output: 'bundle.css' }),
+
+ resolve({
+ browser: true,
+ dedupe: ['svelte']
+ }),
+ commonjs(),
+
+ // Dev server
+ !production && serve(),
+ !production && livereload('public'),
+
+ // Minify in production
+ production && terser()
+ ],
+ watch: {
+ clearScreen: false
+ }
+};
+```
+
+## Best Practices
+
+### Bundle Optimization
+
+1. **Enable tree shaking** - Use ES modules
+2. **Mark side effects** - Set `sideEffects` in package.json
+3. **Use terser** - Minify production builds
+4. **Analyze bundles** - Use rollup-plugin-visualizer
+5. **Code split** - Lazy load routes and features
+
+### External Dependencies
+
+```javascript
+export default {
+ // Don't bundle peer dependencies for libraries
+ external: [
+ 'react',
+ 'react-dom',
+ /^lodash\//
+ ],
+ output: {
+ globals: {
+ react: 'React',
+ 'react-dom': 'ReactDOM'
+ }
+ }
+};
+```
+
+### Development vs Production
+
+```javascript
+const production = !process.env.ROLLUP_WATCH;
+
+export default {
+ plugins: [
+ replace({
+ preventAssignment: true,
+ 'process.env.NODE_ENV': JSON.stringify(
+ production ? 'production' : 'development'
+ )
+ }),
+ production && terser()
+ ].filter(Boolean)
+};
+```
+
+### Error Handling
+
+```javascript
+export default {
+ onwarn(warning, warn) {
+ // Ignore circular dependency warnings
+ if (warning.code === 'CIRCULAR_DEPENDENCY') {
+ return;
+ }
+
+ // Ignore unused external imports
+ if (warning.code === 'UNUSED_EXTERNAL_IMPORT') {
+ return;
+ }
+
+ // Treat other warnings as errors
+ if (warning.code === 'UNRESOLVED_IMPORT') {
+ throw new Error(warning.message);
+ }
+
+ // Use default warning handling
+ warn(warning);
+ }
+};
+```
+
+## Common Patterns
+
+### Library Build
+
+```javascript
+import pkg from './package.json';
+
+export default {
+ input: 'src/index.js',
+ external: Object.keys(pkg.peerDependencies || {}),
+ output: [
+ {
+ file: pkg.main,
+ format: 'cjs',
+ sourcemap: true
+ },
+ {
+ file: pkg.module,
+ format: 'esm',
+ sourcemap: true
+ }
+ ]
+};
+```
+
+### Application Build
+
+```javascript
+export default {
+ input: 'src/main.js',
+ output: {
+ dir: 'dist',
+ format: 'esm',
+ chunkFileNames: 'chunks/[name]-[hash].js',
+ entryFileNames: '[name]-[hash].js',
+ sourcemap: true
+ },
+ plugins: [
+ // All dependencies bundled
+ resolve({ browser: true }),
+ commonjs(),
+ terser()
+ ]
+};
+```
+
+### Web Worker Build
+
+```javascript
+export default [
+ // Main application
+ {
+ input: 'src/main.js',
+ output: {
+ file: 'dist/main.js',
+ format: 'esm'
+ },
+ plugins: [resolve(), commonjs()]
+ },
+ // Web worker (IIFE format)
+ {
+ input: 'src/worker.js',
+ output: {
+ file: 'dist/worker.js',
+ format: 'iife'
+ },
+ plugins: [resolve(), commonjs()]
+ }
+];
+```
+
+## Troubleshooting
+
+### Common Issues
+
+**Module not found:**
+- Check @rollup/plugin-node-resolve is configured
+- Verify package is installed
+- Check `external` array
+
+**CommonJS module issues:**
+- Add @rollup/plugin-commonjs
+- Check `namedExports` configuration
+- Try `transformMixedEsModules: true`
+
+**Circular dependencies:**
+- Use `onwarn` to suppress or fix
+- Refactor to break cycles
+- Check import order
+
+**Sourcemaps not working:**
+- Set `sourcemap: true` in output
+- Ensure plugins pass through maps
+- Check browser devtools settings
+
+**Large bundle size:**
+- Use rollup-plugin-visualizer
+- Check for duplicate dependencies
+- Verify tree shaking is working
+- Mark unused packages as external
+
+## CLI Reference
+
+```bash
+# Basic build
+rollup -c
+
+# Watch mode
+rollup -c -w
+
+# Custom config
+rollup -c rollup.custom.config.js
+
+# Output format
+rollup src/main.js --format esm --file dist/bundle.js
+
+# Environment variables
+NODE_ENV=production rollup -c
+
+# Silent mode
+rollup -c --silent
+
+# Generate bundle stats
+rollup -c --perf
+```
+
+## References
+
+- **Rollup Documentation**: https://rollupjs.org
+- **Plugin Directory**: https://github.com/rollup/plugins
+- **Awesome Rollup**: https://github.com/rollup/awesome
+- **GitHub**: https://github.com/rollup/rollup
+
+## Related Skills
+
+- **svelte** - Using Rollup with Svelte
+- **typescript** - TypeScript compilation with Rollup
+- **nostr-tools** - Bundling Nostr applications
diff --git a/.claude/skills/svelte/SKILL.md b/.claude/skills/svelte/SKILL.md
new file mode 100644
index 0000000..0d87233
--- /dev/null
+++ b/.claude/skills/svelte/SKILL.md
@@ -0,0 +1,1004 @@
+---
+name: svelte
+description: This skill should be used when working with Svelte 3/4, including components, reactivity, stores, lifecycle, and component communication. Provides comprehensive knowledge of Svelte patterns, best practices, and reactive programming concepts.
+---
+
+# Svelte 3/4 Skill
+
+This skill provides comprehensive knowledge and patterns for working with Svelte effectively in modern web applications.
+
+## When to Use This Skill
+
+Use this skill when:
+- Building Svelte applications and components
+- Working with Svelte reactivity and stores
+- Implementing component communication patterns
+- Managing component lifecycle
+- Optimizing Svelte application performance
+- Troubleshooting Svelte-specific issues
+- Working with Svelte transitions and animations
+- Integrating with external libraries
+
+## Core Concepts
+
+### Svelte Overview
+
+Svelte is a compiler-based frontend framework that:
+- **Compiles to vanilla JavaScript** - No runtime library shipped to browser
+- **Reactive by default** - Variables are reactive, assignments trigger updates
+- **Component-based** - Single-file components with `.svelte` extension
+- **CSS scoping** - Styles are scoped to components by default
+- **Built-in transitions** - Animation primitives included
+- **Two-way binding** - Simple data binding with `bind:`
+
+### Component Structure
+
+Svelte components have three sections:
+
+```svelte
+
+
+
+
+
+
+ Clicked {count} times
+
+```
+
+## Reactivity
+
+### Reactive Declarations
+
+Use `$:` for reactive statements and computed values:
+
+```svelte
+
+```
+
+### Reactive Assignments
+
+Reactivity is triggered by assignments:
+
+```svelte
+
+```
+
+**Key Points:**
+- Array methods like `push`, `pop` need reassignment to trigger updates
+- Object property changes need reassignment
+- Use spread operator for immutable updates
+
+## Props
+
+### Declaring Props
+
+```svelte
+
+
+{greeting}, {name}!
+```
+
+### Spread Props
+
+```svelte
+
+
+
+
+
+
+```
+
+### Prop Types with JSDoc
+
+```svelte
+
+```
+
+## Events
+
+### DOM Events
+
+```svelte
+
+
+
+Click me
+
+
+ console.log('clicked')}>Click
+
+
+Submit
+Once
+
+
+
+```
+
+### Component Events
+
+Dispatch custom events from components:
+
+```svelte
+
+
+
+ handleSelect('foo')}>
+ Select
+
+```
+
+```svelte
+
+
+
+
+```
+
+### Event Forwarding
+
+```svelte
+
+Click me
+
+
+
+```
+
+## Bindings
+
+### Two-Way Binding
+
+```svelte
+
+
+
+
+
+
+
+
+
+ A
+ B
+
+
+
+
+
+
+ A
+ B
+
+
+
+
+```
+
+### Component Bindings
+
+```svelte
+
+
+
+
+
+```
+
+### Element Bindings
+
+```svelte
+
+
+
+
+
+
+
+ {divWidth} x {divHeight}
+
+```
+
+## Stores
+
+### Writable Stores
+
+```javascript
+// stores.js
+import { writable } from 'svelte/store';
+
+export const count = writable(0);
+
+// With custom methods
+function createCounter() {
+ const { subscribe, set, update } = writable(0);
+
+ return {
+ subscribe,
+ increment: () => update(n => n + 1),
+ decrement: () => update(n => n - 1),
+ reset: () => set(0)
+ };
+}
+
+export const counter = createCounter();
+```
+
+```svelte
+
+
+Count: {$count}
+ $count += 1}>Increment
+
+Counter: {$counter}
+Increment
+```
+
+### Readable Stores
+
+```javascript
+import { readable } from 'svelte/store';
+
+// Time store that updates every second
+export const time = readable(new Date(), function start(set) {
+ const interval = setInterval(() => {
+ set(new Date());
+ }, 1000);
+
+ return function stop() {
+ clearInterval(interval);
+ };
+});
+```
+
+### Derived Stores
+
+```javascript
+import { derived } from 'svelte/store';
+import { time } from './stores.js';
+
+export const elapsed = derived(
+ time,
+ $time => Math.round(($time - start) / 1000)
+);
+
+// Derived from multiple stores
+export const combined = derived(
+ [storeA, storeB],
+ ([$a, $b]) => $a + $b
+);
+
+// Async derived
+export const asyncDerived = derived(
+ source,
+ ($source, set) => {
+ fetch(`/api/${$source}`)
+ .then(r => r.json())
+ .then(set);
+ },
+ 'loading...' // initial value
+);
+```
+
+### Store Contract
+
+Any object with a `subscribe` method is a store:
+
+```javascript
+// Custom store implementation
+function createCustomStore(initial) {
+ let value = initial;
+ const subscribers = new Set();
+
+ return {
+ subscribe(fn) {
+ subscribers.add(fn);
+ fn(value);
+ return () => subscribers.delete(fn);
+ },
+ set(newValue) {
+ value = newValue;
+ subscribers.forEach(fn => fn(value));
+ }
+ };
+}
+```
+
+## Lifecycle
+
+### Lifecycle Functions
+
+```svelte
+
+```
+
+**Key Points:**
+- `onMount` runs only in browser, not during SSR
+- `onMount` callbacks must be called during component initialization
+- Use `tick()` to wait for pending state changes to apply to DOM
+
+## Logic Blocks
+
+### If Blocks
+
+```svelte
+{#if condition}
+ Condition is true
+{:else if otherCondition}
+ Other condition is true
+{:else}
+ Neither condition is true
+{/if}
+```
+
+### Each Blocks
+
+```svelte
+{#each items as item}
+ {item.name}
+{/each}
+
+
+{#each items as item, index}
+ {index}: {item.name}
+{/each}
+
+
+{#each items as item (item.id)}
+ {item.name}
+{/each}
+
+
+{#each items as { id, name }}
+ {id}: {name}
+{/each}
+
+
+{#each items as item}
+ {item.name}
+{:else}
+ No items
+{/each}
+```
+
+### Await Blocks
+
+```svelte
+{#await promise}
+ Loading...
+{:then value}
+ The value is {value}
+{:catch error}
+ Error: {error.message}
+{/await}
+
+
+{#await promise then value}
+ The value is {value}
+{/await}
+```
+
+### Key Blocks
+
+Force component recreation when value changes:
+
+```svelte
+{#key value}
+
+{/key}
+```
+
+## Slots
+
+### Basic Slots
+
+```svelte
+
+
+
+
+ No content provided
+
+
+```
+
+```svelte
+
+ Card content
+
+```
+
+### Named Slots
+
+```svelte
+
+
+
+
+
+
+
+
+```
+
+```svelte
+
+ Page Title
+ Main content
+ Footer content
+
+```
+
+### Slot Props
+
+```svelte
+
+
+ {#each items as item}
+
+
+ {item.name}
+
+
+ {/each}
+
+```
+
+```svelte
+
+ {index}: {item.name}
+
+```
+
+## Transitions and Animations
+
+### Transitions
+
+```svelte
+
+
+
+{#if visible}
+ Fades in and out
+{/if}
+
+
+{#if visible}
+
+ Flies in
+
+{/if}
+
+
+{#if visible}
+
+ Different transitions
+
+{/if}
+
+
+{#if visible}
+
+ Slides with easing
+
+{/if}
+```
+
+### Custom Transitions
+
+```javascript
+function typewriter(node, { speed = 1 }) {
+ const valid = node.childNodes.length === 1
+ && node.childNodes[0].nodeType === Node.TEXT_NODE;
+
+ if (!valid) {
+ throw new Error('This transition only works on text nodes');
+ }
+
+ const text = node.textContent;
+ const duration = text.length / (speed * 0.01);
+
+ return {
+ duration,
+ tick: t => {
+ const i = Math.trunc(text.length * t);
+ node.textContent = text.slice(0, i);
+ }
+ };
+}
+```
+
+### Animations
+
+Animate elements when they move within an each block:
+
+```svelte
+
+
+{#each items as item (item.id)}
+
+ {item.name}
+
+{/each}
+```
+
+## Actions
+
+Reusable element-level logic:
+
+```svelte
+
+
+ visible = false}>
+ Click outside to close
+
+
+
+ Hover for tooltip
+
+```
+
+## Special Elements
+
+### svelte:component
+
+Dynamic component rendering:
+
+```svelte
+
+
+
+```
+
+### svelte:element
+
+Dynamic HTML elements:
+
+```svelte
+
+
+Dynamic heading
+```
+
+### svelte:window
+
+```svelte
+
+
+
+```
+
+### svelte:body and svelte:head
+
+```svelte
+
+
+
+ Page Title
+
+
+```
+
+### svelte:options
+
+```svelte
+
+```
+
+## Context API
+
+Share data between components without prop drilling:
+
+```svelte
+
+
+```
+
+```svelte
+
+
+
+Current theme: {theme.color}
+```
+
+**Key Points:**
+- Context is not reactive by default
+- Use stores in context for reactive values
+- Context is available only during component initialization
+
+## Best Practices
+
+### Component Design
+
+1. **Keep components focused** - Single responsibility
+2. **Use composition** - Prefer slots over complex props
+3. **Extract logic to stores** - Shared state in stores
+4. **Use actions for DOM logic** - Reusable element behaviors
+5. **Type with JSDoc** - Document prop types
+
+### Reactivity
+
+1. **Understand triggers** - Assignments trigger updates
+2. **Use immutable patterns** - Spread for arrays/objects
+3. **Avoid side effects in reactive statements** - Keep them pure
+4. **Use derived stores** - For computed values from stores
+
+### Performance
+
+1. **Key each blocks** - Use unique keys for list items
+2. **Use immutable option** - When data is immutable
+3. **Lazy load components** - Dynamic imports
+4. **Minimize store subscriptions** - Unsubscribe when done
+
+### State Management
+
+1. **Local state first** - Component variables for local state
+2. **Stores for shared state** - Cross-component communication
+3. **Context for configuration** - Theme, i18n, etc.
+4. **Custom stores for logic** - Encapsulate complex state
+
+## Common Patterns
+
+### Async Data Loading
+
+```svelte
+
+
+{#if loading}
+ Loading...
+{:else if error}
+ Error: {error.message}
+{:else}
+ {data}
+{/if}
+```
+
+### Form Handling
+
+```svelte
+
+
+
+```
+
+### Modal Pattern
+
+```svelte
+
+
+
+{#if open}
+
+{/if}
+```
+
+## Troubleshooting
+
+### Common Issues
+
+**Reactivity not working:**
+- Check for proper assignment (reassign arrays/objects)
+- Use `$:` for derived values
+- Store subscriptions need `$` prefix
+
+**Component not updating:**
+- Verify prop changes trigger parent re-render
+- Check key blocks for forced recreation
+- Use `{#key}` to force component recreation
+
+**Memory leaks:**
+- Clean up subscriptions in `onDestroy`
+- Return cleanup functions from `onMount`
+- Unsubscribe from stores manually if not using `$`
+
+**Styles not applying:**
+- Check for `:global()` if targeting child components
+- Verify CSS specificity
+- Use `class:` directive properly
+
+## References
+
+- **Svelte Documentation**: https://svelte.dev/docs
+- **Svelte Tutorial**: https://svelte.dev/tutorial
+- **Svelte REPL**: https://svelte.dev/repl
+- **Svelte Society**: https://sveltesociety.dev
+- **GitHub**: https://github.com/sveltejs/svelte
+
+## Related Skills
+
+- **rollup** - Bundling Svelte applications
+- **nostr-tools** - Nostr integration in Svelte apps
+- **typescript** - TypeScript with Svelte
diff --git a/.gitignore b/.gitignore
index c3728cb..6f9843b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -79,6 +79,7 @@ cmd/benchmark/data
!*.svelte
!.github/**
!.github/workflows/**
+!.claude/**
!app/web/dist/**
!app/web/dist/*.js
!app/web/dist/*.js.map