# Npub Cash Address

> Use when implementing npub.cash static Lightning address functionality - provides complete patterns for obtaining static Lightning addresses (username@npubx.cash), managing npub.cash account settings, syncing quotes from npub.cash, and integrating with Lightning wallets

- Skill: `majiayu000/npub-cash-address` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds add majiayu000/npub-cash-address`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/npub-cash-address/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/majiayu000/npub-cash-address

---


# npub.cash Static Lightning Address Implementation

## Overview

Complete implementation guide for integrating with npub.cash to obtain static Lightning addresses for users. npub.cash provides a service that generates static Lightning addresses in the format `username@npubx.cash` based on a user's Nostr public key, allowing users to receive Lightning payments at a permanent address.

**Core Capabilities:**
- Obtain static Lightning address from npub.cash (`username@npubx.cash`)
- Manage npub.cash account settings (mint URL, lock quotes)
- Sync quotes from npub.cash to Cashu wallet
- Real-time quote subscription and updates
- NIP-98 authentication with npub.cash API
- Automatic mint management when syncing quotes

**Note:** This skill is **optional** for `lightning-wallet` implementations. Users can optionally use npub.cash to get a static Lightning address, but it's not required for basic Lightning wallet functionality.

## Prerequisites

**IMPORTANT:** Before adding dependencies, review your project's `package.json` to check if any of these packages already exist. If they do, verify the versions are compatible with the requirements below. Only add packages that are missing or need version updates.

**Required packages:**
- `npubcash-sdk@^0.2.0` - Official npub.cash SDK (minimum version 0.2.0, later versions acceptable)
- `npubcash-types` - TypeScript types for npub.cash (if not included in SDK)
- `nostr-tools@^2.13.0` - Nostr protocol utilities (for NIP-98 authentication)

**Required skills (must be referenced/implemented):**
- `qr-code-generator` - QR code generation for displaying Lightning addresses (see `qr-code-generator` skill)
- `lightning-wallet` - Lightning wallet operations (optional integration target)

**Optional dependencies:**
- NIP-98 authentication hook (for Nostr-based authentication)

## Implementation Checklist

- [ ] Set up NIP-98 authentication with npub.cash
- [ ] Initialize npubcash-sdk client
- [ ] Implement user info retrieval (get static Lightning address)
- [ ] Implement QR code generation for Lightning address (see `qr-code-generator` skill)
- [ ] Implement copy to clipboard functionality (see `bitcoin-wallet` skill for copy hook)
- [ ] Implement mint URL management
- [ ] Implement quote syncing from npub.cash
- [ ] Add real-time quote subscription
- [ ] Integrate with Lightning wallet (optional)
- [ ] Add error handling and user feedback

## Part 1: Understanding npub.cash Static Addresses

### Static Address vs Dynamic Invoices

**Static Lightning Address:**
- Format: `username@npubx.cash`
- Permanent address tied to user's Nostr pubkey
- Others can send payments to this address at any time
- npub.cash generates invoices on-demand when payments are received
- User doesn't need to create invoices manually

**Dynamic Invoices:**
- Created on-demand by user
- Expire after a set time
- User must generate and share each invoice
- More control but less convenient for recurring payments

### npub.cash Service

npub.cash provides:
- **Static addresses**: `username@npubx.cash` format
- **Quote management**: Tracks pending Lightning payments
- **Mint integration**: Links user's preferred Cashu mint
- **Real-time updates**: WebSocket subscription for quote updates

### Difference from lightning-address Skill

**This skill (`npub-cash-address`):**
- **Obtains** a static Lightning address from npub.cash
- Creates/manages the user's address on npub.cash
- Syncs quotes from npub.cash to wallet

**`lightning-address` skill:**
- **Resolves** any Lightning address to an invoice
- Takes an address (like `alice@strike.me`) and gets an invoice
- Works with any Lightning address provider

## Part 2: Core Implementation

### NIP-98 Authentication Setup

**Set up NIP-98 authentication for npub.cash API:**

```typescript
// hooks/useNIP98.ts (prerequisite)
import { useCallback } from 'react';
import { useCurrentUser } from './useCurrentUser';
import type { NostrEvent } from '@nostrify/nostrify';

export interface NIP98Options {
  url: string;
  method: string;
  body?: unknown;
}

/**
 * Hook for creating NIP-98 HTTP Authentication headers
 * Based on https://github.com/nostr-protocol/nips/blob/master/98.md
 */
export function useNIP98() {
  const { user } = useCurrentUser();

  /**
   * Create a NIP-98 authorization header for HTTP requests
   * @param options - The request details (url, method, optional body)
   * @returns Promise<string> - The "Nostr base64..." authorization header value
   */
  const createAuthHeader = useCallback(async (options: NIP98Options): Promise<string> => {
    if (!user) {
      throw new Error('User must be logged in to create NIP-98 auth header');
    }

    const { url, method, body } = options;
    
    // Parse URL to remove query parameters (per NIP-98 spec)
    const urlObj = new URL(url);
    const urlWithoutQuery = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`;

    const tags: string[][] = [
      ['u', urlWithoutQuery],
      ['method', method.toUpperCase()],
    ];

    // If there's a request body (POST, PUT, PATCH), add payload hash
    if (body && (method === 'POST' || method === 'PUT' || method === 'PATCH')) {
      const bodyString = JSON.stringify(body);
      const encoder = new TextEncoder();
      const data = encoder.encode(bodyString);
      const hashBuffer = await crypto.subtle.digest('SHA-256', data);
      const payloadHash = Array.from(new Uint8Array(hashBuffer))
        .map(b => b.toString(16).padStart(2, '0'))
        .join('');
      
      tags.push(['payload', payloadHash]);
    }

    // Create and sign the NIP-98 event (kind 27235)
    const event = await user.signer.signEvent({
      kind: 27235,
      content: '',
      tags,
      created_at: Math.floor(Date.now() / 1000),
    }) as NostrEvent;

    // Encode the event as base64
    const eventJson = JSON.stringify(event);
    const base64Event = btoa(eventJson);

    return `Nostr ${base64Event}`;
  }, [user]);

  return {
    createAuthHeader,
    isLoggedIn: !!user,
    pubkey: user?.pubkey,
    signer: user?.signer,
  };
}
```

**Key Points:**
- **NIP-98 Event**: Creates a kind 27235 event with URL and method tags
- **URL Parsing**: Removes query parameters per NIP-98 specification
- **Payload Hashing**: For POST/PUT/PATCH requests, includes SHA-256 hash of request body
- **Base64 Encoding**: Encodes signed event as base64 for HTTP Authorization header
- **Format**: Returns `"Nostr {base64Event}"` format for Authorization header

### npub.cash Hook Implementation

**Complete hook for npub.cash integration:**

```typescript
// hooks/wallet/useNpubCash.ts
import { useCallback, useState, useMemo, useRef } from 'react';
import { useNIP98 } from '../useNIP98';
import { NPCClient, JWTAuthProvider } from 'npubcash-sdk';
import type { Quote } from 'npubcash-types';
import type { Manager } from 'coco-cashu-core';
import type { EventTemplate, Event } from 'nostr-tools';
// optional import { useToast } from '@/hooks/useToast';

const NPUBCASH_BASE_URL = 'https://npubx.cash';

export interface NpubCashUserInfo {
  pubkey: string;
  name?: string;
  mintUrl: string; // API returns camelCase
  lockQuote: boolean; // API returns camelCase
}

/**
 * Hook for interacting with npub.cash API using the official SDK
 */
export function useNpubCash() {
  const { createAuthHeader, signer, isLoggedIn, pubkey } = useNIP98();
  // Optional: User feedback notifications
  // Option 1: Console logging
  // const logMessage = (message: string) => console.log(message);
  // Option 2: Toast notifications (if useToast hook is available)
  // const { toast } = useToast();
  // Option 3: No notification handler
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  // Ref to track last sync timestamp for incremental updates
  const lastSyncRef = useRef<number>(0);

  // Create NPCClient instance using the SDK with JWTAuthProvider for proper WebSocket auth
  const npcClient = useMemo(() => {
    if (!isLoggedIn || !signer) return null;
    // Adapt NostrSigner to JWTAuthProvider's expected SigningFunc signature
    const signingFunc = async (eventTemplate: EventTemplate): Promise<Event> => {
      return await signer.signEvent(eventTemplate);
    };
    const auth = new JWTAuthProvider(NPUBCASH_BASE_URL, signingFunc);
    return new NPCClient(NPUBCASH_BASE_URL, auth);
  }, [isLoggedIn, signer]);

  // Generic wrapper for SDK calls with loading state
  const withLoading = useCallback(async <T>(operation: () => Promise<T>): Promise<T | null> => {
    try {
      setIsLoading(true);
      setError(null);
      return await operation();
    } catch (err) {
      const errorMessage = err instanceof Error ? err.message : 'Unknown error';
      setError(errorMessage);
      return null;
    } finally {
      setIsLoading(false);
    }
  }, []);

  const setMint = useCallback(async (mintUrl: string): Promise<boolean> => {
    if (!npcClient) return false;
    const result = await withLoading(() => npcClient.settings.setMintUrl(mintUrl));
    return result !== null;
  }, [npcClient, withLoading]);

  const getUserInfo = useCallback(async (): Promise<NpubCashUserInfo | null> => {
    if (!npcClient) return null;
    
    // Since SDK doesn't have getUserInfo, we'll use manual implementation
    return withLoading(async () => {
      const url = `${NPUBCASH_BASE_URL}/api/v2/user/info`;
      const authHeader = await createAuthHeader({ url, method: 'GET' });
      
      const response = await fetch(url, {
        method: 'GET',
        headers: { 'Authorization': authHeader, 'Content-Type': 'application/json' },
      });

      if (!response.ok) {
        const errorData = await response.json().catch(() => ({}));
        throw new Error(errorData.message || `Failed to get user info (${response.status})`);
      }

      const data = await response.json();
      return data.data.user;
    });
  }, [npcClient, createAuthHeader, withLoading]);

  const getAllQuotes = useCallback(async (): Promise<Quote[]> => {
    if (!npcClient) return [];
    const result = await withLoading(() => npcClient.getAllQuotes());
    return result || [];
  }, [npcClient, withLoading]);

  const getQuotesSince = useCallback(async (since: number): Promise<Quote[]> => {
    if (!npcClient) return [];
    const result = await withLoading(() => npcClient.getQuotesSince(since));
    return result || [];
  }, [npcClient, withLoading]);

  const setLockQuotes = useCallback(async (lockQuotes: boolean): Promise<boolean> => {
    if (!npcClient) return false;
    const result = await withLoading(() => npcClient.settings.setLock(lockQuotes));
    return result !== null;
  }, [npcClient, withLoading]);

  const subscribeToQuotes = useCallback((
    onUpdate: (quoteId: string) => void,
    onError?: (msg: string) => void
  ) => {
    if (!npcClient) return () => {};
    return npcClient.subscribe(onUpdate, onError);
  }, [npcClient]);

  const clearError = useCallback(() => setError(null), []);

  return {
    // Core methods
    setMint,
    getUserInfo,
    
    // Quote management methods
    getAllQuotes,
    getQuotesSince,
    setLockQuotes,
    subscribeToQuotes,
    
    // Status
    isLoading,
    error,
    isLoggedIn,
    pubkey,
    clearError,
    
    // SDK instance for advanced usage
    npcClient,
  };
}
```

## Part 3: Getting Static Lightning Address

### Retrieving User Info

**Get user's static Lightning address from npub.cash:**

```typescript
// In your component
const { getUserInfo, isLoading, error } = useNpubCash();

// Get user info (includes static address)
const userInfo = await getUserInfo();

if (userInfo) {
  // Extract username from pubkey or use name if available
  const username = userInfo.name || extractUsernameFromPubkey(userInfo.pubkey);
  const staticAddress = `${username}@npubx.cash`;
  
  console.log('Static Lightning address:', staticAddress);
  // Example: "alice@npubx.cash"
}
```

### Displaying Static Address

**Show static address in wallet UI with QR code and copy functionality:**

```typescript
// components/StaticAddressDisplay.tsx
import { useEffect, useState } from 'react';
import { useNpubCash } from '@/hooks/wallet/useNpubCash';
import { useQRCodeGenerator } from '@/hooks/useQRCodeGenerator';
import { useCopyToClipboard } from '@/hooks/useCopyToClipboard';
import { Button } from '@/components/ui/button';
import { Copy, Check } from 'lucide-react';
import { QRModal } from '@/components/QRModal';

export function StaticAddressDisplay() {
  const { getUserInfo, isLoading } = useNpubCash();
  const { generateQRCode, isGenerating: isGeneratingQR } = useQRCodeGenerator();
  const { copy, copied } = useCopyToClipboard();
  const [staticAddress, setStaticAddress] = useState<string | null>(null);
  const [qrCodeUrl, setQrCodeUrl] = useState<string>('');
  const [showQRModal, setShowQRModal] = useState(false);

  useEffect(() => {
    const fetchAddress = async () => {
      const userInfo = await getUserInfo();
      if (userInfo) {
        const username = userInfo.name || extractUsernameFromPubkey(userInfo.pubkey);
        const address = `${username}@npubx.cash`;
        setStaticAddress(address);
        
        // Generate QR code for the address
        const qrUrl = await generateQRCode(address);
        setQrCodeUrl(qrUrl);
      }
    };
    
    fetchAddress();
  }, [getUserInfo, generateQRCode]);

  if (isLoading) {
    return <div>Loading address...</div>;
  }

  if (!staticAddress) {
    return <div>No address available</div>;
  }

  return (
    <div className="space-y-4">
      <h3>Your Lightning Address</h3>
      
      {/* Address display with copy button */}
      <div className="flex items-center gap-2 p-3 bg-muted rounded-lg">
        <code className="flex-1 text-sm break-all">{staticAddress}</code>
        <Button
          variant="ghost"
          size="icon"
          onClick={() => copy(staticAddress)}
          className="h-8 w-8 shrink-0"
          aria-label="Copy address"
        >
          {copied ? (
            <Check className="h-4 w-4 text-green-600" />
          ) : (
            <Copy className="h-4 w-4" />
          )}
        </Button>
      </div>

      {/* QR code display */}
      {qrCodeUrl && (
        <div className="flex flex-col items-center gap-2">
          <img
            src={qrCodeUrl}
            alt="QR Code"
            className="w-48 h-48 cursor-pointer"
            onClick={() => setShowQRModal(true)}
          />
          <Button
            variant="outline"
            onClick={() => setShowQRModal(true)}
            disabled={isGeneratingQR}
          >
            {isGeneratingQR ? 'Generating...' : 'View QR Code'}
          </Button>
        </div>
      )}

      {/* QR Modal */}
      <QRModal
        isOpen={showQRModal}
        onClose={() => setShowQRModal(false)}
        qrCodeUrl={qrCodeUrl}
        content={staticAddress}
        title="Lightning Address QR Code"
        description="Scan this QR code to send Lightning payments"
      />
    </div>
  );
}
```

**Key Features:**
- **QR Code Generation** - Uses `qr-code-generator` skill to create QR codes for the Lightning address
- **Copy to Clipboard** - Uses copy hook from `bitcoin-wallet` skill for easy address copying
- **Visual Feedback** - Shows checkmark when address is copied
- **QR Modal** - Full-screen QR code view with copy functionality
- **Accessibility** - Proper ARIA labels and keyboard navigation

### Username Extraction

**Extract username from Nostr pubkey:**

```typescript
// lib/npubCashUtils.ts
export function extractUsernameFromPubkey(pubkey: string): string {
  // npub.cash uses a deterministic username based on pubkey
  // This is typically the first 8-12 characters of the npub
  // You may need to check npub.cash API documentation for exact format
  
  // Example: Use first part of pubkey as username
  // In practice, npub.cash may provide this in the user info
  return pubkey.substring(0, 8);
}

// Or use the name from userInfo if available
const username = userInfo.name || extractUsernameFromPubkey(userInfo.pubkey);
```

## Part 4: Managing npub.cash Settings

### Setting Mint URL

**Configure user's preferred mint on npub.cash:**

```typescript
// In your component
const { setMint, isLoading } = useNpubCash();

// Set mint URL (links npub.cash to user's preferred Cashu mint)
const success = await setMint('https://mint.example.com');

if (success) {
  // Optional: User feedback - choose one:
  // Option 1: Console logging
  // console.log('Mint Updated: Your npub.cash mint has been updated');
  // Option 2: Toast notification (if toast is available)
  // toast({ title: 'Mint Updated', description: 'Your npub.cash mint has been updated' });
  // Option 3: No notification (silent success)
}
```

### Locking Quotes

**Prevent quote modifications (optional security feature):**

```typescript
// In your component
const { setLockQuotes, isLoading } = useNpubCash();

// Lock quotes to prevent modifications
const success = await setLockQuotes(true);

if (success) {
  // Optional: User feedback - choose one:
  // Option 1: Console logging
  // console.log('Quotes Locked: Your quotes are now locked');
  // Option 2: Toast notification (if toast is available)
  // toast({ title: 'Quotes Locked', description: 'Your quotes are now locked' });
  // Option 3: No notification (silent success)
}
```

## Part 5: Quote Syncing with Lightning Wallet

### Syncing Quotes from npub.cash

**Sync quotes from npub.cash to Cashu wallet:**

```typescript
// hooks/wallet/useNpubCash.ts (excerpt)
const refreshQuotes = useCallback(async (coco: Manager): Promise<void> => {
  if (!npcClient || !coco) return;
  
  try {
    // Fetch all quotes from npub.cash
    const quotes = await getAllQuotes();
    
    if (quotes.length === 0) {
      // Optional: User feedback - choose one:
      // Option 1: Console logging
      // console.log('No Quotes Found: No pending quotes found on npub.cash');
      // Option 2: Toast notification (if toast is available)
      // toast({ title: "No Quotes Found", description: "No pending quotes found on npub.cash" });
      // Option 3: No notification (silent)
      return;
    }
    
    // Group quotes by mint URL
    const quotesByMint = quotes.reduce((acc, quote) => {
      if (!acc[quote.mintUrl]) {
        acc[quote.mintUrl] = [];
      }
      acc[quote.mintUrl].push(quote);
      return acc;
    }, {} as Record<string, typeof quotes>);
    
    let addedQuotes = 0;
    
    // Process each mint's quotes
    for (const [mintUrl, mintQuotes] of Object.entries(quotesByMint)) {
      try {
        // Check if mint exists (known) by checking all mints
        const allMints = await coco.mint.getAllMints();
        const isKnown = allMints.some(m => m.mintUrl.toLowerCase() === mintUrl.toLowerCase());
        
        if (!isKnown) {
          await coco.mint.addMint(mintUrl, { trusted: true });
        } else {
          // Mint exists, ensure it's trusted
          const isTrusted = await coco.mint.isTrustedMint(mintUrl);
          if (!isTrusted) {
            await coco.mint.trustMint(mintUrl);
          }
        }
        
        // Add quotes to the wallet
        coco.quotes.addMintQuote(
          mintUrl,
          mintQuotes.map((q) => ({
            ...q,
            expiry: q.expiresAt,
            quote: q.quoteId,
            state: "PAID",
            unit: "sat",
          })),
        );
        
        addedQuotes += mintQuotes.length;
      } catch (err) {
        console.warn(`Failed to process quotes for mint ${mintUrl}:`, err);
      }
    }
    
    if (addedQuotes > 0) {
      // Optional: User feedback - choose one:
      // Option 1: Console logging
      // console.log(`Quotes Refreshed: Successfully added ${addedQuotes} quotes from npub.cash`);
      // Option 2: Toast notification (if toast is available)
      // toast({ title: "Quotes Refreshed", description: `Successfully added ${addedQuotes} quotes from npub.cash` });
      // Option 3: No notification (silent success)
    }
  } catch (err) {
    // Optional: User feedback - choose one:
    // Option 1: Console logging
    // console.error('Quote Refresh Failed:', err instanceof Error ? err.message : 'Unknown error');
    // Option 2: Toast notification (if toast is available)
    // toast({ variant: "destructive", title: "Refresh Failed", description: err instanceof Error ? err.message : 'Failed to refresh quotes' });
    // Option 3: No notification (silent failure)
  }
}, [npcClient, getAllQuotes]);
```

### Real-Time Quote Subscription

**Subscribe to real-time quote updates:**

```typescript
// hooks/wallet/useNpubCash.ts (excerpt)
const syncQuotesWithWallet = useCallback(async (coco: Manager): Promise<() => void> => {
  if (!npcClient || !coco) return () => {};
  
  try {
    // Fetch and sync all quotes initially
    const quotes = await getAllQuotes();
    if (quotes.length > 0) {
      // ... sync initial quotes (see refreshQuotes implementation)
    }
    
    // Subscribe to real-time updates
    const unsubscribe = subscribeToQuotes(
      async (quoteId: string) => {
        try {
          // When a quote is updated, fetch newer quotes and sync
          const recentQuotes = await getQuotesSince(lastSyncRef.current);
          if (recentQuotes.length > 0) {
            // ... sync recent quotes (see refreshQuotes implementation)
            lastSyncRef.current = Math.floor(Date.now() / 1000);
          }
        } catch (err) {
          console.error('Failed to sync quote update:', err);
        }
      },
      (err: string) => {
        console.error('Quote subscription error:', err);
        // Optional: User feedback - choose one:
        // Option 1: Console logging
        // console.error('Sync Error: Failed to sync quotes from npub.cash');
        // Option 2: Toast notification (if toast is available)
        // toast({ variant: "destructive", title: "Sync Error", description: "Failed to sync quotes from npub.cash" });
        // Option 3: No notification (silent failure)
      }
    );
    
    return unsubscribe;
  } catch (err) {
    console.error('Failed to set up quote sync:', err);
    return () => {};
  }
}, [npcClient, getAllQuotes, getQuotesSince, subscribeToQuotes]);
```

**Usage in component:**

```typescript
// In your component
const { syncQuotesWithWallet } = useNpubCash();
const { coco } = useCashu(); // From lightning-wallet skill

useEffect(() => {
  if (!coco || !isLoggedIn) return;
  
  // Set up real-time quote syncing
  const unsubscribe = await syncQuotesWithWallet(coco);
  
  return () => {
    if (unsubscribe) unsubscribe();
  };
}, [coco, isLoggedIn, syncQuotesWithWallet]);
```

## Part 6: Integration with Lightning Wallet

### Optional Integration

**This skill is optional for `lightning-wallet` implementations:**

```typescript
// components/wallet/WalletHeader.tsx (excerpt)
import { useNpubCash } from '@/hooks/wallet/useNpubCash';

export function WalletHeader({ userPubkey, mode }: WalletHeaderProps) {
  const { getUserInfo, isLoading } = useNpubCash();
  const [npubCashUsername, setNpubCashUsername] = useState<string | null>(null);

  useEffect(() => {
    if (userPubkey && mode === 'lightning') {
      // Optionally fetch npub.cash address
      const fetchAddress = async () => {
        const userInfo = await getUserInfo();
        if (userInfo) {
          const username = userInfo.name || extractUsernameFromPubkey(userInfo.pubkey);
          setNpubCashUsername(username);
        }
      };
      fetchAddress();
    }
  }, [userPubkey, mode, getUserInfo]);

  return (
    <div>
      {/* ... other wallet header content ... */}
      
      {/* Optionally show static address if available */}
      {userPubkey && mode === 'lightning' && npubCashUsername && (
        <div className="mt-3 px-4 w-full max-w-[400px] mx-auto">
          <LightningAddressDisplay
            lightningAddress={`${npubCashUsername}@npubx.cash`}
          />
        </div>
      )}
    </div>
  );
}
```

### Syncing Mint Settings

**Sync mint URL between wallet and npub.cash:**

```typescript
// When user changes active mint in wallet
const { setMint } = useNpubCash();
const { activeMintUrl, setActiveMintUrl } = useMintManager({ /* ... */ });

const handleMintChange = async (mintUrl: string) => {
  // Update wallet mint
  await setActiveMintUrl(mintUrl);
  
  // Optionally sync to npub.cash
  if (isNpubCashLoggedIn) {
    await setMint(mintUrl);
  }
};
```

## Part 7: Error Handling

### Authentication Errors

**Handle NIP-98 authentication failures:**

```typescript
const { getUserInfo, error, isLoggedIn } = useNpubCash();

if (!isLoggedIn) {
  return (
    <div>
      <p>Please log in with Nostr to use npub.cash</p>
      <LoginArea />
    </div>
  );
}

if (error) {
  if (error.includes('401') || error.includes('Unauthorized')) {
    // Optional: User feedback - choose one:
    // Option 1: Console logging
    // console.error('Authentication Failed: Please log in again');
    // Option 2: Toast notification (if toast is available)
    // toast({ variant: 'destructive', title: 'Authentication Failed', description: 'Please log in again' });
    // Option 3: No notification (silent failure)
  }
}
```

### API Errors

**Handle npub.cash API errors:**

```typescript
try {
  const userInfo = await getUserInfo();
  if (!userInfo) {
    // Optional: User feedback - choose one:
    // Option 1: Console logging
    // console.error('Failed to Get Address: Could not retrieve your npub.cash address');
    // Option 2: Toast notification (if toast is available)
    // toast({ variant: 'destructive', title: 'Failed to Get Address', description: 'Could not retrieve your npub.cash address' });
    // Option 3: No notification (silent failure)
  }
} catch (err) {
  const errorMessage = err instanceof Error ? err.message : 'Unknown error';
  
  if (errorMessage.includes('404')) {
    // User may not have npub.cash account yet
    // Optional: User feedback - choose one:
    // Option 1: Console logging
    // console.log('No Account: You may need to create an npub.cash account first');
    // Option 2: Toast notification (if toast is available)
    // toast({ title: 'No Account', description: 'You may need to create an npub.cash account first' });
    // Option 3: No notification (silent)
  } else {
    // Optional: User feedback - choose one:
    // Option 1: Console logging
    // console.error('API Error:', errorMessage);
    // Option 2: Toast notification (if toast is available)
    // toast({ variant: 'destructive', title: 'Error', description: errorMessage });
    // Option 3: No notification (silent failure)
  }
}
```

## Part 8: Best Practices

### Lazy Loading

**Only fetch npub.cash data when needed:**

```typescript
// Don't fetch on every render
const [userInfo, setUserInfo] = useState<NpubCashUserInfo | null>(null);
const [hasFetched, setHasFetched] = useState(false);

const fetchUserInfo = useCallback(async () => {
  if (hasFetched) return; // Already fetched
  const info = await getUserInfo();
  setUserInfo(info);
  setHasFetched(true);
}, [getUserInfo, hasFetched]);

// Only fetch when user explicitly requests it or when component mounts
useEffect(() => {
  if (shouldShowAddress) {
    fetchUserInfo();
  }
}, [shouldShowAddress, fetchUserInfo]);
```

### Caching User Info

**Cache user info to avoid repeated API calls:**

```typescript
// Use React Query or similar for caching
import { useQuery } from '@tanstack/react-query';

const { data: userInfo } = useQuery({
  queryKey: ['npubcash-user-info', pubkey],
  queryFn: () => getUserInfo(),
  enabled: !!pubkey && isLoggedIn,
  staleTime: 5 * 60 * 1000, // Cache for 5 minutes
});
```

### Cleanup Subscriptions

**Always cleanup WebSocket subscriptions:**

```typescript
useEffect(() => {
  if (!coco || !isLoggedIn) return;
  
  const unsubscribe = await syncQuotesWithWallet(coco);
  
  return () => {
    if (unsubscribe) unsubscribe();
  };
}, [coco, isLoggedIn, syncQuotesWithWallet]);
```

## Part 9: Common Pitfalls

### 1. ❌ Not checking authentication

**Problem:** Attempting to use npub.cash without Nostr login causes errors.

**Solution:** Always check authentication:
```typescript
if (!isLoggedIn) {
  return <LoginPrompt />;
}
```

### 2. ❌ Not handling missing user info

**Problem:** Assuming user always has npub.cash account.

**Solution:** Handle null user info gracefully:
```typescript
const userInfo = await getUserInfo();
if (!userInfo) {
  // User may not have npub.cash account
  return <CreateAccountPrompt />;
}
```

### 3. ❌ Not cleaning up subscriptions

**Problem:** Memory leaks from uncleaned WebSocket subscriptions.

**Solution:** Always return cleanup function:
```typescript
useEffect(() => {
  const unsubscribe = await syncQuotesWithWallet(coco);
  return () => unsubscribe();
}, [coco, syncQuotesWithWallet]);
```

### 4. ❌ Not syncing mint settings

**Problem:** npub.cash and wallet use different mints.

**Solution:** Sync mint URL when user changes it:
```typescript
const handleMintChange = async (mintUrl: string) => {
  await setActiveMintUrl(mintUrl);
  if (isNpubCashLoggedIn) {
    await setMint(mintUrl); // Sync to npub.cash
  }
};
```

## Security Considerations

1. **NIP-98 Authentication**: Always use proper NIP-98 authentication for API calls
2. **Token Storage**: Don't store authentication tokens in localStorage
3. **Error Messages**: Don't expose internal API errors to users
4. **Quote Validation**: Validate quotes from npub.cash before adding to wallet
5. **Mint Trust**: Only trust mints from verified sources

## Verification Checklist

- [ ] NIP-98 authentication works correctly
- [ ] User info retrieval returns static address
- [ ] Static address displays correctly in UI
- [ ] Mint URL can be set and updated
- [ ] Quote syncing works (manual refresh)
- [ ] Real-time quote subscription works
- [ ] Quotes are properly added to wallet
- [ ] Unknown mints are automatically added
- [ ] WebSocket subscriptions are cleaned up
- [ ] Error handling covers all failure cases
- [ ] Integration with Lightning wallet works (optional)

## Summary

To implement npub.cash static Lightning address functionality:

1. **Set up NIP-98 authentication** - Required for npub.cash API access
2. **Initialize npubcash-sdk** - Use official SDK with JWTAuthProvider
3. **Get user info** - Retrieve static Lightning address (`username@npubx.cash`)
4. **Display address** - Show address in wallet UI with copy/QR functionality
5. **Manage settings** - Sync mint URL and lock quotes (optional)
6. **Sync quotes** - Fetch and sync quotes from npub.cash to wallet
7. **Subscribe to updates** - Use WebSocket for real-time quote updates
8. **Integrate with Lightning wallet** - Optional integration for enhanced UX

**Key principle:** This skill is for **obtaining** static Lightning addresses from npub.cash. Use the `lightning-address` skill to **resolve** any Lightning address (including npub.cash addresses) to invoices for sending payments.


