Skip to main content
Glama

getSentZaps

Retrieve zap payments sent by a Nostr user to track outgoing financial interactions on the decentralized social network.

Instructions

Get zaps sent 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:374-376 (registration)
    Tool registration in main index.ts file, referencing the schema and handler function.
    "getSentZaps",
    "Get zaps sent by a public key",
    getSentZapsToolConfig,
  • Core handler function: fetches potential zap receipts (prioritizing #P:sender tag), processes each to determine direction relative to pubkey, filters for sent/self zaps, optionally validates per NIP-57, computes totals, formats detailed output.
    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 sent zaps for ${hexPubkey} from ${relaysToUse.join(", ")}`);
        
        // First try the direct and correct approach: query with uppercase 'P' tag (NIP-57)
        if (debug) console.error("Trying direct query with #P tag...");
        
        let potentialSentZaps: NostrEvent[] = [];
        try {
          potentialSentZaps = await pool.querySync(
            relaysToUse,
            {
              kinds: [KINDS.ZapReceipt],
              "#P": [hexPubkey], // uppercase 'P' for sender
              limit: Math.ceil(limit * 1.5), // Fetch a bit more to account for potential invalid zaps
            } as NostrFilter,
            { timeout: QUERY_TIMEOUT }
          );
          if (debug) console.error(`Direct #P tag query returned ${potentialSentZaps.length} results`);
        } catch (e: unknown) {
          if (debug) console.error(`Direct #P tag query failed: ${e instanceof Error ? e.message : String(e)}`);
        }
        
        // If the direct query didn't return enough results, try the fallback method
        if (!potentialSentZaps || potentialSentZaps.length < limit) {
          if (debug) console.error("Direct query yielded insufficient results, trying fallback approach...");
          
          // Try a fallback approach - fetch a larger set of zap receipts
          const additionalZaps = await pool.querySync(
            relaysToUse,
            {
              kinds: [KINDS.ZapReceipt],
              limit: Math.max(limit * 10, 100), // Get a larger sample
            } as NostrFilter,
            { timeout: QUERY_TIMEOUT }
          );
          
          if (debug) {
            console.error(`Retrieved ${additionalZaps?.length || 0} additional zap receipts to analyze`);
          }
          
          if (additionalZaps && additionalZaps.length > 0) {
            // Add these to our potential sent zaps
            potentialSentZaps = [...potentialSentZaps, ...additionalZaps];
          }
        }
        
        if (!potentialSentZaps || potentialSentZaps.length === 0) {
            return {
              content: [
                {
                  type: "text",
                  text: "No zap receipts found to analyze",
                },
              ],
            };
          }
          
        // Process and filter zaps
        let processedZaps: any[] = [];
        let invalidCount = 0;
        let nonSentCount = 0;
        
        if (debug) {
          console.error(`Processing ${potentialSentZaps.length} potential sent zaps...`);
        }
        
        // Process each zap to determine if it was sent by the target pubkey
        for (const zap of potentialSentZaps) {
          try {
            // Process the zap receipt with context of the target pubkey
            const processedZap = processZapReceipt(zap as ZapReceipt, hexPubkey);
            
            // Skip zaps that aren't sent by this pubkey
            if (processedZap.direction !== 'sent' && processedZap.direction !== 'self') {
              if (debug) {
                console.error(`Skipping zap ${zap.id.slice(0, 8)}... with direction ${processedZap.direction}`);
              }
              nonSentCount++;
              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);
            }
          }
        }
        
        // Deduplicate by zap ID
        const uniqueZaps = new Map<string, any>();
        processedZaps.forEach(zap => uniqueZaps.set(zap.id, zap));
        processedZaps = Array.from(uniqueZaps.values());
        
        if (processedZaps.length === 0) {
          let message = `No zaps sent by ${displayPubkey} were found.`;
          if (invalidCount > 0 || nonSentCount > 0) {
            message += ` (${invalidCount} invalid zaps and ${nonSentCount} non-sent zaps were filtered out)`;
          }
          message += " This could be because:\n1. The user hasn't sent any zaps\n2. The zap receipts don't properly contain the sender's pubkey\n3. The relays queried don't have this data";
          
          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 sent
        const totalSats = processedZaps.reduce((sum, zap) => sum + (zap.amountSats || 0), 0);
        
        // For debugging, examine the first zap in detail
        if (debug && processedZaps.length > 0) {
          const firstZap = processedZaps[0];
          console.error("Sample sent zap:", JSON.stringify(firstZap, null, 2));
        }
        
        const formattedZaps = processedZaps.map(zap => formatZapReceipt(zap, hexPubkey)).join("\n");
        
        return {
          content: [
            {
              type: "text",
              text: `Found ${processedZaps.length} zaps sent by ${displayPubkey}.\nTotal sent: ${totalSats} sats\n\n${formattedZaps}`,
            },
          ],
        };
      } catch (error) {
        console.error("Error fetching sent zaps:", error);
        
        return {
          content: [
            {
              type: "text",
              text: `Error fetching sent zaps for ${displayPubkey}: ${error instanceof Error ? error.message : "Unknown error"}`,
            },
          ],
        };
      } finally {
        // Clean up any subscriptions and close the pool
        await pool.close();
      }
    },
  • Zod input schema defining parameters for the getSentZaps tool.
    export const getSentZapsToolConfig = {
      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"),
    };
  • Key helper: enriches and caches zap receipts, determines direction ('sent' via 'P' tag or zap request pubkey), extracts sats amount, targets.
    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' });
      }
    }
  • Crucial helper for identifying sent zaps: checks uppercase 'P' sender tag first, falls back to zap request in description.
    export function determineZapDirection(zapReceipt: ZapReceipt, pubkey: string): ZapDirection {
      try {
        // Check if received via lowercase 'p' tag (recipient)
        const isReceived = zapReceipt.tags.some(tag => tag[0] === 'p' && tag[1] === pubkey);
        
        // Check if sent via uppercase 'P' tag (sender, per NIP-57)
        let isSent = zapReceipt.tags.some(tag => tag[0] === 'P' && tag[1] === pubkey);
        
        if (!isSent) {
          // Fallback: check description tag for the sender pubkey
          const descriptionTag = zapReceipt.tags.find(tag => tag[0] === "description" && tag.length > 1);
          if (descriptionTag && descriptionTag[1]) {
            try {
              const zapRequest: ZapRequest = JSON.parse(descriptionTag[1]);
              isSent = zapRequest && zapRequest.pubkey === pubkey;
            } catch (e) {
              // Ignore parsing errors
            }
          }
        }
        
        // Determine direction
        if (isSent && isReceived) {
          return 'self';
        } else if (isSent) {
          return 'sent';
        } else if (isReceived) {
          return 'received';
        } else {
          return 'unknown';
        }
      } catch (error) {
        console.error("Error determining zap direction:", error);
        return 'unknown';
      }
    }

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