Agent Skills: npub.cash Static Lightning Address Implementation

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

UncategorizedID: PerceptLabs/nostril/npub-cash-address

Install this agent skill to your local

pnpm dlx add-skill https://github.com/PerceptLabs/nostril/tree/HEAD/skills/npub-cash-address

Skill Files

Browse the full folder contents for npub-cash-address.

Download Skill

Loading file tree…

skills/npub-cash-address/SKILL.md

Skill Metadata

Name
npub-cash-address
Description
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

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:

// 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:

// 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:

// 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:

// 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:

// 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:

// 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):

// 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:

// 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:

// 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:

// 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:

// 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:

// 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:

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:

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:

// 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:

// 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:

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:

if (!isLoggedIn) {
  return <LoginPrompt />;
}

2. ❌ Not handling missing user info

Problem: Assuming user always has npub.cash account.

Solution: Handle null user info gracefully:

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:

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:

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.