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
| Name | Required | Description | Default |
|---|---|---|---|
| pubkey | Yes | Public key of the Nostr user (hex format or npub format) | |
| limit | No | Maximum number of zaps to fetch | |
| relays | No | Optional list of relays to query | |
| validateReceipts | No | Whether to validate zap receipts according to NIP-57 | |
| debug | No | Enable 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,
- index.ts:377-561 (handler)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(); } },
- zap/zap-tools.ts:526-532 (schema)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"), };
- zap/zap-tools.ts:349-391 (helper)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' }); } }
- zap/zap-tools.ts:311-346 (helper)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'; } }