Skip to main content
Glama

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
NameRequiredDescriptionDefault
pubkeyYesPublic key of the Nostr user (hex format or npub format)
limitNoMaximum number of zaps to fetch
relaysNoOptional list of relays to query
validateReceiptsNoWhether to validate zap receipts according to NIP-57
debugNoEnable 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(); } }, );
  • 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(); } },
  • 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"), };
  • 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' }); } }
  • 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)}` }; } }

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