Skip to main content
Glama

getAllZaps

Retrieve all sent and received zap payments for a Nostr user by providing their public key, with options to limit results and validate receipts.

Instructions

Get all zaps (sent and received) for a public key

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pubkeyYesPublic key of the Nostr user (hex format or npub format)
limitNoMaximum number of total zaps to fetch
relaysNoOptional list of relays to query
validateReceiptsNoWhether to validate zap receipts according to NIP-57
debugNoEnable verbose debug logging

Implementation Reference

  • Primary handler implementation for the 'getAllZaps' tool. Queries Nostr relays for zap receipts (kind 9735) where the pubkey appears as recipient (#p tag) or sender (#P tag), deduplicates, processes each with helpers to determine direction/amount/target, optionally validates per NIP-57, computes sent/received/self stats and net balance, formats output, limits to requested number.
    server.tool(
      "getAllZaps",
      "Get all zaps (sent and received) for a public key",
      getAllZapsToolConfig,
      async ({ pubkey, limit, relays, validateReceipts, debug }) => {
        // Convert npub to hex if needed
        const hexPubkey = npubToHex(pubkey);
        if (!hexPubkey) {
          return {
            content: [
              {
                type: "text",
                text: "Invalid public key format. Please provide a valid hex pubkey or npub.",
              },
            ],
          };
        }
        
        // Generate a friendly display version of the pubkey
        const displayPubkey = formatPubkey(hexPubkey);
        
        const relaysToUse = relays || DEFAULT_RELAYS;
        // Create a fresh pool for this request
        const pool = getFreshPool(relaysToUse);
        
        try {
          console.error(`Fetching all zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`);
          
          // Use a more efficient approach: fetch all potentially relevant zaps in parallel
          
          // Prepare all required queries in parallel to reduce total time
          const fetchPromises = [
            // 1. Fetch received zaps (lowercase 'p' tag)
            pool.querySync(
            relaysToUse,
            {
              kinds: [KINDS.ZapReceipt],
              "#p": [hexPubkey],
                limit: Math.ceil(limit * 1.5),
            } as NostrFilter,
            { timeout: QUERY_TIMEOUT }
            ),
            
            // 2. Fetch sent zaps (uppercase 'P' tag)
            pool.querySync(
            relaysToUse,
            {
              kinds: [KINDS.ZapReceipt],
              "#P": [hexPubkey],
                limit: Math.ceil(limit * 1.5),
            } as NostrFilter,
            { timeout: QUERY_TIMEOUT }
            )
          ];
          
          // Add a general query if we're in debug mode or need more comprehensive results
          if (debug) {
            fetchPromises.push(
              pool.querySync(
              relaysToUse,
              {
                kinds: [KINDS.ZapReceipt],
                  limit: Math.max(limit * 5, 50),
              } as NostrFilter,
              { timeout: QUERY_TIMEOUT }
              )
            );
          }
          
          // Execute all queries in parallel
          const results = await Promise.allSettled(fetchPromises);
          
          // Collect all zaps from successful queries
          const allZaps: NostrEvent[] = [];
          
          results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
              const zaps = result.value as NostrEvent[];
            if (debug) {
                const queryTypes = ['Received', 'Sent', 'General'];
                console.error(`${queryTypes[index]} query returned ${zaps.length} results`);
              }
              allZaps.push(...zaps);
            } else if (debug) {
              const queryTypes = ['Received', 'Sent', 'General'];
              console.error(`${queryTypes[index]} query failed:`, result.reason);
            }
          });
          
          if (allZaps.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: `No zaps found for ${displayPubkey}. Try specifying different relays that might have the data.`,
                },
              ],
            };
          }
          
          if (debug) {
            console.error(`Retrieved ${allZaps.length} total zaps before deduplication`);
          }
          
          // Deduplicate by zap ID
          const uniqueZapsMap = new Map<string, NostrEvent>();
          allZaps.forEach(zap => uniqueZapsMap.set(zap.id, zap));
          const uniqueZaps = Array.from(uniqueZapsMap.values());
          
          if (debug) {
            console.error(`Deduplicated to ${uniqueZaps.length} unique zaps`);
          }
          
          // Process each zap to determine its relevance to the target pubkey
          let processedZaps: any[] = [];
          let invalidCount = 0;
          let irrelevantCount = 0;
          
          for (const zap of uniqueZaps) {
            try {
              // Process the zap with the target pubkey as context
              const processedZap = processZapReceipt(zap as ZapReceipt, hexPubkey);
              
              // Skip zaps that are neither sent nor received by this pubkey
              if (processedZap.direction === 'unknown') {
                  if (debug) {
                  console.error(`Skipping irrelevant zap ${zap.id.slice(0, 8)}...`);
                }
                irrelevantCount++;
                continue;
              }
              
              // Validate if requested
              if (validateReceipts) {
                const validationResult = validateZapReceipt(zap);
                if (!validationResult.valid) {
                if (debug) {
                    console.error(`Invalid zap receipt ${zap.id.slice(0, 8)}...: ${validationResult.reason}`);
                }
                  invalidCount++;
                  continue;
              }
              }
            
              processedZaps.push(processedZap);
            } catch (error) {
            if (debug) {
                console.error(`Error processing zap ${zap.id.slice(0, 8)}...`, error);
              }
            }
          }
          
          if (processedZaps.length === 0) {
            let message = `No relevant zaps found for ${displayPubkey}.`;
            if (invalidCount > 0 || irrelevantCount > 0) {
              message += ` (${invalidCount} invalid zaps and ${irrelevantCount} irrelevant zaps were filtered out)`;
            }
            
            return {
              content: [
                {
                  type: "text",
                  text: message,
                },
              ],
            };
          }
          
          // Sort zaps by created_at in descending order (newest first)
          processedZaps.sort((a, b) => b.created_at - a.created_at);
          
          // Calculate statistics: sent, received, and self zaps
          const sentZaps = processedZaps.filter(zap => zap.direction === 'sent');
          const receivedZaps = processedZaps.filter(zap => zap.direction === 'received');
          const selfZaps = processedZaps.filter(zap => zap.direction === 'self');
          
          // Calculate total sats
          const totalSent = sentZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
          const totalReceived = receivedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
          const totalSelfZaps = selfZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
          
          // Limit to requested number for display
          processedZaps = processedZaps.slice(0, limit);
          
          // Format the zaps with the pubkey context
          const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n");
          
          // Prepare summary statistics
          const summary = [
            `Zap Summary for ${displayPubkey}:`,
            `- ${sentZaps.length} zaps sent (${totalSent} sats)`,
            `- ${receivedZaps.length} zaps received (${totalReceived} sats)`,
            `- ${selfZaps.length} self-zaps (${totalSelfZaps} sats)`,
            `- Net balance: ${totalReceived - totalSent} sats`,
            `\nShowing ${processedZaps.length} most recent zaps:\n`
          ].join("\n");
          
          return {
            content: [
              {
                type: "text",
                text: `${summary}\n${formattedZaps}`,
              },
            ],
          };
        } catch (error) {
          console.error("Error fetching all zaps:", error);
          
          return {
            content: [
              {
                type: "text",
                text: `Error fetching all zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`,
              },
            ],
          };
        } finally {
          // Clean up any subscriptions and close the pool
          await pool.close();
        }
      },
    );
  • Zod schema defining input parameters and validation for the getAllZaps tool.
    export const getAllZapsToolConfig = {
      pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"),
      limit: z.number().min(1).max(100).default(20).describe("Maximum number of total zaps to fetch"),
      relays: z.array(z.string()).optional().describe("Optional list of relays to query"),
      validateReceipts: z.boolean().default(true).describe("Whether to validate zap receipts according to NIP-57"),
      debug: z.boolean().default(false).describe("Enable verbose debug logging"),
    };
  • index.ts:564-785 (registration)
    MCP tool registration call that registers the 'getAllZaps' tool with name, description, schema, and inline handler function.
    server.tool(
      "getAllZaps",
      "Get all zaps (sent and received) for a public key",
      getAllZapsToolConfig,
      async ({ pubkey, limit, relays, validateReceipts, debug }) => {
        // Convert npub to hex if needed
        const hexPubkey = npubToHex(pubkey);
        if (!hexPubkey) {
          return {
            content: [
              {
                type: "text",
                text: "Invalid public key format. Please provide a valid hex pubkey or npub.",
              },
            ],
          };
        }
        
        // Generate a friendly display version of the pubkey
        const displayPubkey = formatPubkey(hexPubkey);
        
        const relaysToUse = relays || DEFAULT_RELAYS;
        // Create a fresh pool for this request
        const pool = getFreshPool(relaysToUse);
        
        try {
          console.error(`Fetching all zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`);
          
          // Use a more efficient approach: fetch all potentially relevant zaps in parallel
          
          // Prepare all required queries in parallel to reduce total time
          const fetchPromises = [
            // 1. Fetch received zaps (lowercase 'p' tag)
            pool.querySync(
            relaysToUse,
            {
              kinds: [KINDS.ZapReceipt],
              "#p": [hexPubkey],
                limit: Math.ceil(limit * 1.5),
            } as NostrFilter,
            { timeout: QUERY_TIMEOUT }
            ),
            
            // 2. Fetch sent zaps (uppercase 'P' tag)
            pool.querySync(
            relaysToUse,
            {
              kinds: [KINDS.ZapReceipt],
              "#P": [hexPubkey],
                limit: Math.ceil(limit * 1.5),
            } as NostrFilter,
            { timeout: QUERY_TIMEOUT }
            )
          ];
          
          // Add a general query if we're in debug mode or need more comprehensive results
          if (debug) {
            fetchPromises.push(
              pool.querySync(
              relaysToUse,
              {
                kinds: [KINDS.ZapReceipt],
                  limit: Math.max(limit * 5, 50),
              } as NostrFilter,
              { timeout: QUERY_TIMEOUT }
              )
            );
          }
          
          // Execute all queries in parallel
          const results = await Promise.allSettled(fetchPromises);
          
          // Collect all zaps from successful queries
          const allZaps: NostrEvent[] = [];
          
          results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
              const zaps = result.value as NostrEvent[];
            if (debug) {
                const queryTypes = ['Received', 'Sent', 'General'];
                console.error(`${queryTypes[index]} query returned ${zaps.length} results`);
              }
              allZaps.push(...zaps);
            } else if (debug) {
              const queryTypes = ['Received', 'Sent', 'General'];
              console.error(`${queryTypes[index]} query failed:`, result.reason);
            }
          });
          
          if (allZaps.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: `No zaps found for ${displayPubkey}. Try specifying different relays that might have the data.`,
                },
              ],
            };
          }
          
          if (debug) {
            console.error(`Retrieved ${allZaps.length} total zaps before deduplication`);
          }
          
          // Deduplicate by zap ID
          const uniqueZapsMap = new Map<string, NostrEvent>();
          allZaps.forEach(zap => uniqueZapsMap.set(zap.id, zap));
          const uniqueZaps = Array.from(uniqueZapsMap.values());
          
          if (debug) {
            console.error(`Deduplicated to ${uniqueZaps.length} unique zaps`);
          }
          
          // Process each zap to determine its relevance to the target pubkey
          let processedZaps: any[] = [];
          let invalidCount = 0;
          let irrelevantCount = 0;
          
          for (const zap of uniqueZaps) {
            try {
              // Process the zap with the target pubkey as context
              const processedZap = processZapReceipt(zap as ZapReceipt, hexPubkey);
              
              // Skip zaps that are neither sent nor received by this pubkey
              if (processedZap.direction === 'unknown') {
                  if (debug) {
                  console.error(`Skipping irrelevant zap ${zap.id.slice(0, 8)}...`);
                }
                irrelevantCount++;
                continue;
              }
              
              // Validate if requested
              if (validateReceipts) {
                const validationResult = validateZapReceipt(zap);
                if (!validationResult.valid) {
                if (debug) {
                    console.error(`Invalid zap receipt ${zap.id.slice(0, 8)}...: ${validationResult.reason}`);
                }
                  invalidCount++;
                  continue;
              }
              }
            
              processedZaps.push(processedZap);
            } catch (error) {
            if (debug) {
                console.error(`Error processing zap ${zap.id.slice(0, 8)}...`, error);
              }
            }
          }
          
          if (processedZaps.length === 0) {
            let message = `No relevant zaps found for ${displayPubkey}.`;
            if (invalidCount > 0 || irrelevantCount > 0) {
              message += ` (${invalidCount} invalid zaps and ${irrelevantCount} irrelevant zaps were filtered out)`;
            }
            
            return {
              content: [
                {
                  type: "text",
                  text: message,
                },
              ],
            };
          }
          
          // Sort zaps by created_at in descending order (newest first)
          processedZaps.sort((a, b) => b.created_at - a.created_at);
          
          // Calculate statistics: sent, received, and self zaps
          const sentZaps = processedZaps.filter(zap => zap.direction === 'sent');
          const receivedZaps = processedZaps.filter(zap => zap.direction === 'received');
          const selfZaps = processedZaps.filter(zap => zap.direction === 'self');
          
          // Calculate total sats
          const totalSent = sentZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
          const totalReceived = receivedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
          const totalSelfZaps = selfZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
          
          // Limit to requested number for display
          processedZaps = processedZaps.slice(0, limit);
          
          // Format the zaps with the pubkey context
          const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n");
          
          // Prepare summary statistics
          const summary = [
            `Zap Summary for ${displayPubkey}:`,
            `- ${sentZaps.length} zaps sent (${totalSent} sats)`,
            `- ${receivedZaps.length} zaps received (${totalReceived} sats)`,
            `- ${selfZaps.length} self-zaps (${totalSelfZaps} sats)`,
            `- Net balance: ${totalReceived - totalSent} sats`,
            `\nShowing ${processedZaps.length} most recent zaps:\n`
          ].join("\n");
          
          return {
            content: [
              {
                type: "text",
                text: `${summary}\n${formattedZaps}`,
              },
            ],
          };
        } catch (error) {
          console.error("Error fetching all zaps:", error);
          
          return {
            content: [
              {
                type: "text",
                text: `Error fetching all zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`,
              },
            ],
          };
        } finally {
          // Clean up any subscriptions and close the pool
          await pool.close();
        }
      },
    );
  • Key helper function called by handler for each zap receipt. Enriches raw events with direction (sent/received/self/unknown), amount in sats, targets, using cache for efficiency.
    export function processZapReceipt(zapReceipt: ZapReceipt, pubkey: string): CachedZap {
      // Check if we already have this zap in the cache
      const existingCachedZap = zapCache.get(zapReceipt.id);
      if (existingCachedZap) {
        return existingCachedZap;
      }
      
      try {
        // Determine direction relative to the specified pubkey
        const direction = determineZapDirection(zapReceipt, pubkey);
        
        // Extract target pubkey (recipient)
        const targetPubkey = zapReceipt.tags.find(tag => tag[0] === 'p' && tag.length > 1)?.[1];
        
        // Extract target event if any
        const targetEvent = zapReceipt.tags.find(tag => tag[0] === 'e' && tag.length > 1)?.[1];
        
        // Extract target coordinate if any (a tag)
        const targetCoordinate = zapReceipt.tags.find(tag => tag[0] === 'a' && tag.length > 1)?.[1];
        
        // Parse zap request to get additional data
        const zapRequestData = parseZapRequestData(zapReceipt);
        
        // Decode bolt11 invoice to get amount
        const decodedInvoice = decodeBolt11FromZap(zapReceipt);
        const amountSats = decodedInvoice ? 
          getAmountFromDecodedInvoice(decodedInvoice) : 
          (zapRequestData?.amount ? Math.floor(zapRequestData.amount / 1000) : undefined);
        
        // Create enriched zap and add to cache
        return zapCache.add(zapReceipt, {
          direction,
          amountSats,
          targetPubkey,
          targetEvent,
          targetCoordinate
        });
      } catch (error) {
        console.error("Error processing zap receipt:", error);
        // Still cache the basic zap with unknown direction
        return zapCache.add(zapReceipt, { direction: 'unknown' });
      }
    }
  • Helper function used by handler to format each processed zap into a detailed readable string for the final output.
    export function formatZapReceipt(zap: NostrEvent, pubkeyContext?: string): string {
      if (!zap) return "";
      
      try {
        // Cast to ZapReceipt for better type safety since we know we're dealing with kind 9735
        const zapReceipt = zap as ZapReceipt;
        
        // Process the zap receipt with context if provided
        let enrichedZap: CachedZap;
        if (pubkeyContext) {
          enrichedZap = processZapReceipt(zapReceipt, pubkeyContext);
        } else {
          // Check if it's already in cache
          const cachedZap = zapCache.get(zapReceipt.id);
          if (cachedZap) {
            enrichedZap = cachedZap;
          } else {
            // Process without context - won't have direction information
            enrichedZap = {
              ...zapReceipt,
              processedAt: Date.now()
            };
          }
        }
        
        // Get basic zap info
        const created = new Date(zapReceipt.created_at * 1000).toLocaleString();
        
        // Get sender information from P tag or description
        let sender = "Unknown";
        let senderPubkey: string | undefined;
        const senderPTag = zapReceipt.tags.find(tag => tag[0] === 'P' && tag.length > 1);
        if (senderPTag && senderPTag[1]) {
          senderPubkey = senderPTag[1];
          const npub = hexToNpub(senderPubkey);
          sender = npub ? `${npub.slice(0, 8)}...${npub.slice(-4)}` : `${senderPubkey.slice(0, 8)}...${senderPubkey.slice(-8)}`;
        } else {
          // Try to get from description
          const zapRequestData = parseZapRequestData(zapReceipt);
          if (zapRequestData?.pubkey) {
            senderPubkey = zapRequestData.pubkey;
            const npub = hexToNpub(senderPubkey);
            sender = npub ? `${npub.slice(0, 8)}...${npub.slice(-4)}` : `${senderPubkey.slice(0, 8)}...${senderPubkey.slice(-8)}`;
          }
        }
        
        // Get recipient information
        const recipient = zapReceipt.tags.find(tag => tag[0] === 'p' && tag.length > 1)?.[1];
        let formattedRecipient = "Unknown";
        if (recipient) {
          const npub = hexToNpub(recipient);
          formattedRecipient = npub ? `${npub.slice(0, 8)}...${npub.slice(-4)}` : `${recipient.slice(0, 8)}...${recipient.slice(-8)}`;
        }
        
        // Get amount
        let amount: string = enrichedZap.amountSats !== undefined ? 
          `${enrichedZap.amountSats} sats` : 
          "Unknown";
        
        // Get comment
        let comment = "No comment";
        const zapRequestData = parseZapRequestData(zapReceipt);
        if (zapRequestData?.content) {
          comment = zapRequestData.content;
        }
        
        // Check if this zap is for a specific event or coordinate
        let zapTarget = "User";
        let targetId = "";
        
        if (enrichedZap.targetEvent) {
          zapTarget = "Event";
          targetId = enrichedZap.targetEvent;
        } else if (enrichedZap.targetCoordinate) {
          zapTarget = "Replaceable Event";
          targetId = enrichedZap.targetCoordinate;
        }
        
        // Format the output with all available information
        const lines = [
          `From: ${sender}`,
          `To: ${formattedRecipient}`,
          `Amount: ${amount}`,
          `Created: ${created}`,
          `Target: ${zapTarget}${targetId ? ` (${targetId.slice(0, 8)}...)` : ''}`,
          `Comment: ${comment}`,
        ];
        
        // Add payment preimage if available
        const preimageTag = zapReceipt.tags.find(tag => tag[0] === "preimage" && tag.length > 1);
        if (preimageTag && preimageTag[1]) {
          lines.push(`Preimage: ${preimageTag[1].slice(0, 10)}...`);
        }
        
        // Add payment hash if available in bolt11 invoice
        const decodedInvoice = decodeBolt11FromZap(zapReceipt);
        if (decodedInvoice) {
          const paymentHashSection = decodedInvoice.sections.find((section: any) => section.name === "payment_hash");
          if (paymentHashSection) {
            lines.push(`Payment Hash: ${paymentHashSection.value.slice(0, 10)}...`);
          }
        }
        
        // Add direction information if available
        if (enrichedZap.direction && enrichedZap.direction !== 'unknown') {
          const directionLabels = {
            'sent': '↑ SENT',
            'received': '↓ RECEIVED',
            'self': '↻ SELF ZAP'
          };
          
          lines.unshift(`[${directionLabels[enrichedZap.direction]}]`);
        }
        
        lines.push('---');
        
        return lines.join("\n");
      } catch (error) {
        console.error("Error formatting zap receipt:", error);
        return "Error formatting zap receipt";
      }
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AustinKelsay/nostr-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server