Skip to main content
Glama

getSentZaps

Fetch zaps sent by a Nostr user using their public key, with options to set query limits, specify relays, and validate zap receipts according to NIP-57.

Instructions

Get zaps sent by a public key

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
debugNoEnable verbose debug logging
limitNoMaximum number of zaps to fetch
pubkeyYesPublic key of the Nostr user (hex format or npub format)
relaysNoOptional list of relays to query
validateReceiptsNoWhether to validate zap receipts according to NIP-57

Implementation Reference

  • Primary handler implementation for getSentZaps tool. Converts pubkey, creates Nostr pool, queries relays for kind 9735 with #P:[pubkey] (sender), falls back to broader query if needed, processes each zap using processZapReceipt to determine direction/amount/target, filters for sent/self zaps, optionally validates per NIP-57, sorts newest first, limits results, calculates total sats, formats 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(); } },
  • index.ts:373-376 (registration)
    MCP tool registration for getSentZaps, specifying name, description, input schema (getSentZapsToolConfig), and inline async handler function.
    server.tool( "getSentZaps", "Get zaps sent by a public key", getSentZapsToolConfig,
  • Zod input schema validation for getSentZaps tool parameters.
    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 function used by handler: processes a raw zap receipt relative to a pubkey - determines direction (sent/received/self), extracts amount from bolt11, target info, caches result.
    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' }); } }
  • Helper to format a processed zap receipt into human-readable multi-line string with all details, used for final output.
    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"; } }

Other Tools

Related Tools

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