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