getReceivedZaps
Retrieve zap payments received by a Nostr user's public key, enabling tracking of incoming payments through the Nostr network with configurable validation and relay options.
Instructions
Get zaps received by 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 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:229-372 (registration)MCP tool registration for 'getReceivedZaps', including inline handler function that implements the core logic: validates pubkey, queries Nostr relays for kind 9735 events with '#p' tag matching pubkey (received zaps), processes/validates using helpers, sorts/formats results with totals.server.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)Inline async handler function executing the tool: fetches zap receipts from relays where pubkey is recipient, processes/validates/filters/formats them.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(); } },
- zap/zap-tools.ts:518-524 (schema)Zod schema defining input parameters for the getReceivedZaps tool.export 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:349-391 (helper)Helper to process a raw zap receipt: determines direction relative to pubkey, extracts amount/target, caches enriched data.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:240-308 (helper)NIP-57 zap receipt validator used in the handler.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)}` }; } }