getReceivedZaps
Retrieve zap payments received by a Nostr user’s public key, with options to limit results, specify relays, and validate receipts according to NIP-57 standards.
Instructions
Get zaps received by a public key
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| debug | No | Enable verbose debug logging | |
| limit | No | Maximum number of 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:229-371 (registration)Registration of the 'getReceivedZaps' tool in the MCP server, including inline handler functionserver.tool( "getReceivedZaps", "Get zaps received by a public key", getReceivedZapsToolConfig, 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 zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`); // Query for received zaps - snstr handles timeout internally const zaps = await pool.querySync( relaysToUse, { kinds: [KINDS.ZapReceipt], "#p": [hexPubkey], // lowercase 'p' for recipient limit: Math.ceil(limit * 1.5), // Fetch a bit more to account for potential invalid zaps } as NostrFilter, { timeout: QUERY_TIMEOUT } ); if (!zaps || zaps.length === 0) { return { content: [ { type: "text", text: `No zaps found for ${displayPubkey}`, }, ], }; } if (debug) { console.error(`Retrieved ${zaps.length} raw zap receipts`); } // Process and optionally validate zaps let processedZaps: any[] = []; let invalidCount = 0; for (const zap of zaps) { try { // Process the zap receipt with context of the target pubkey const processedZap = processZapReceipt(zap as ZapReceipt, hexPubkey); // Skip zaps that aren't actually received by this pubkey if (processedZap.direction !== 'received' && processedZap.direction !== 'self') { if (debug) { console.error(`Skipping zap ${zap.id.slice(0, 8)}... with direction ${processedZap.direction}`); } 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 valid zaps found for ${displayPubkey}`; if (invalidCount > 0) { message += ` (${invalidCount} invalid 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); // Limit to requested number processedZaps = processedZaps.slice(0, limit); // Calculate total sats received const totalSats = processedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0); const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n"); return { content: [ { type: "text", text: `Found ${processedZaps.length} zaps received by ${displayPubkey}.\nTotal received: ${totalSats} sats\n\n${formattedZaps}`, }, ], }; } catch (error) { console.error("Error fetching zaps:", error); return { content: [ { type: "text", text: `Error fetching zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`, }, ], }; } finally { // Clean up any subscriptions and close the pool await pool.close(); } }, );
- index.ts:233-370 (handler)Main execution handler: queries Nostr relays for zap receipts (kind 9735) where pubkey is recipient via 'p' tag, processes/validates/filters using helpers, sorts newest first, formats output with totalsasync ({ 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 zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`); // Query for received zaps - snstr handles timeout internally const zaps = await pool.querySync( relaysToUse, { kinds: [KINDS.ZapReceipt], "#p": [hexPubkey], // lowercase 'p' for recipient limit: Math.ceil(limit * 1.5), // Fetch a bit more to account for potential invalid zaps } as NostrFilter, { timeout: QUERY_TIMEOUT } ); if (!zaps || zaps.length === 0) { return { content: [ { type: "text", text: `No zaps found for ${displayPubkey}`, }, ], }; } if (debug) { console.error(`Retrieved ${zaps.length} raw zap receipts`); } // Process and optionally validate zaps let processedZaps: any[] = []; let invalidCount = 0; for (const zap of zaps) { try { // Process the zap receipt with context of the target pubkey const processedZap = processZapReceipt(zap as ZapReceipt, hexPubkey); // Skip zaps that aren't actually received by this pubkey if (processedZap.direction !== 'received' && processedZap.direction !== 'self') { if (debug) { console.error(`Skipping zap ${zap.id.slice(0, 8)}... with direction ${processedZap.direction}`); } 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 valid zaps found for ${displayPubkey}`; if (invalidCount > 0) { message += ` (${invalidCount} invalid 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); // Limit to requested number processedZaps = processedZaps.slice(0, limit); // Calculate total sats received const totalSats = processedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0); const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n"); return { content: [ { type: "text", text: `Found ${processedZaps.length} zaps received by ${displayPubkey}.\nTotal received: ${totalSats} sats\n\n${formattedZaps}`, }, ], }; } catch (error) { console.error("Error fetching zaps:", error); return { content: [ { type: "text", text: `Error fetching 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:518-524 (schema)Zod input schema/validation for tool parametersexport const getReceivedZapsToolConfig = { pubkey: z.string().describe("Public key of the Nostr user (hex format or npub format)"), limit: z.number().min(1).max(100).default(10).describe("Maximum number of 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:348-391 (helper)Core helper: processes raw zap receipt, determines direction relative to pubkey (received/sent/self), extracts amount/target, caches result// Process a zap receipt into an enriched cached zap 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:393-515 (helper)Formats processed zap receipt into detailed readable text output used in tool response// Helper function to format zap receipt with enhanced information 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"; } }