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
| Name | Required | Description | Default |
|---|---|---|---|
| pubkey | Yes | Public key of the Nostr user (hex format or npub format) | |
| limit | No | Maximum number of total zaps to fetch | |
| relays | No | Optional list of relays to query | |
| validateReceipts | No | Whether to validate zap receipts according to NIP-57 | |
| debug | No | Enable verbose debug logging |
Implementation Reference
- index.ts:564-785 (handler)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(); } }, );
- zap/zap-tools.ts:534-540 (schema)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(); } }, );
- zap/zap-tools.ts:349-391 (helper)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' }); } }
- zap/zap-tools.ts:394-515 (helper)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"; } }