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