getAllZaps
Retrieve all zaps sent and received by a Nostr user using their public key. Specify relays, limit results, and validate receipts for accurate data. Integrates with Nostr MCP Server for streamlined access.
Instructions
Get all zaps (sent and received) for a public key
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| debug | No | Enable verbose debug logging | |
| limit | No | Maximum number of total zaps to fetch | |
| pubkey | Yes | Public key of the Nostr user (hex format or npub format) | |
| relays | No | Optional list of relays to query | |
| validateReceipts | No | Whether to validate zap receipts according to NIP-57 |
Implementation Reference
- index.ts:564-785 (handler)Primary handler implementation for getAllZaps tool. Queries Nostr relays in parallel for received (#p) and sent (#P) zap receipts (kind 9735), deduplicates, processes each to determine direction/amount/target using shared helpers, validates optionally, computes sent/received/self stats and net balance, formats detailed output for top N zaps.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 input schema/validation for getAllZaps tool parameters.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"), };
- zap/zap-tools.ts:349-391 (helper)Core helper that processes a raw zap receipt: determines direction (sent/received/self) relative to pubkey, extracts amount/target details, decodes invoice, enriches data, and caches using ZapCache.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)Formats a processed zap receipt into human-readable multi-line string with all key details and direction indicator.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"; } }
- zap/zap-tools.ts:239-308 (helper)Validates zap receipt per NIP-57 Appendix F: checks structure, tags, recipient match, amount consistency.// Validate a zap receipt according to NIP-57 Appendix F export function validateZapReceipt(zapReceipt: NostrEvent, zapRequest?: ZapRequest): { valid: boolean, reason?: string } { try { // 1. Must be kind 9735 if (zapReceipt.kind !== KINDS.ZapReceipt) { return { valid: false, reason: "Not a zap receipt (kind 9735)" }; } // 2. Must have a bolt11 tag const bolt11Tag = zapReceipt.tags.find(tag => tag[0] === "bolt11" && tag.length > 1); if (!bolt11Tag || !bolt11Tag[1]) { return { valid: false, reason: "Missing bolt11 tag" }; } // 3. Must have a description tag with the zap request const descriptionTag = zapReceipt.tags.find(tag => tag[0] === "description" && tag.length > 1); if (!descriptionTag || !descriptionTag[1]) { return { valid: false, reason: "Missing description tag" }; } // 4. Parse the zap request from the description tag if not provided let parsedZapRequest: ZapRequest; try { parsedZapRequest = zapRequest || JSON.parse(descriptionTag[1]); } catch (e) { return { valid: false, reason: "Invalid zap request JSON in description tag" }; } // 5. Validate the zap request structure if (parsedZapRequest.kind !== KINDS.ZapRequest) { return { valid: false, reason: "Invalid zap request kind" }; } // 6. Check that the p tag from the zap request is included in the zap receipt const requestedRecipientPubkey = parsedZapRequest.tags.find(tag => tag[0] === 'p' && tag.length > 1)?.[1]; const receiptRecipientTag = zapReceipt.tags.find(tag => tag[0] === 'p' && tag.length > 1); if (!requestedRecipientPubkey || !receiptRecipientTag || receiptRecipientTag[1] !== requestedRecipientPubkey) { return { valid: false, reason: "Recipient pubkey mismatch" }; } // 7. Check for optional e tag consistency if present in the zap request const requestEventTag = parsedZapRequest.tags.find(tag => tag[0] === 'e' && tag.length > 1); if (requestEventTag) { const receiptEventTag = zapReceipt.tags.find(tag => tag[0] === 'e' && tag.length > 1); if (!receiptEventTag || receiptEventTag[1] !== requestEventTag[1]) { return { valid: false, reason: "Event ID mismatch" }; } } // 8. Check for optional amount consistency const amountTag = parsedZapRequest.tags.find(tag => tag[0] === 'amount' && tag.length > 1); if (amountTag) { // Decode the bolt11 invoice to verify the amount const decodedInvoice = decodeBolt11FromZap(zapReceipt); if (decodedInvoice) { const invoiceAmountMsats = decodedInvoice.sections.find((s: any) => s.name === "amount")?.value; const requestAmountMsats = parseInt(amountTag[1], 10); if (invoiceAmountMsats && Math.abs(invoiceAmountMsats - requestAmountMsats) > 10) { // Allow small rounding differences return { valid: false, reason: "Amount mismatch between request and invoice" }; } } } return { valid: true }; } catch (error) { return { valid: false, reason: `Validation error: ${error instanceof Error ? error.message : String(error)}` }; } }