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