# Nostr Queries

> Query Nostr events efficiently with useNostr + TanStack Query. Covers the standard useQuery pattern, combining related kinds into a single request to avoid rate limiting, and validating events with required tags or strict schemas.

- Skill: `soapbox-pub/nostr-queries` (Agent Skill)
- Install (CLI): `npx skillmds@latest add soapbox-pub/nostr-queries`
- Raw SKILL.md: https://api.skillmd.com/api/skills/soapbox-pub/nostr-queries/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: soapbox-pub (https://skillmd.com/u/soapbox-pub)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/soapbox-pub/nostr-queries

---


# Querying Nostr Events

Use this skill when building a hook that fetches Nostr events. Covers the standard `useNostr` + `useQuery` pattern, efficient query design (combining kinds to avoid relay round-trips), and event validation for kinds with required tags.

## The Standard Pattern

Combine `useNostr` with TanStack Query in a custom hook. Pass the abort signal from `c.signal` into `nostr.query` so cancelled queries free relay resources:

```typescript
import { useNostr } from '@nostrify/react';
import { useQuery } from '@tanstack/react-query';

function usePosts() {
  const { nostr } = useNostr();

  return useQuery({
    queryKey: ['posts'],
    queryFn: async (c) => {
      const events = await nostr.query(
        [{ kinds: [1], limit: 20 }],
        { signal: c.signal },
      );
      return events;
    },
  });
}
```

Transform events into a domain model inside the `queryFn` if needed — callers should rarely see raw `NostrEvent`s. Multiple calls to `nostr.query()` inside one `queryFn` are fine for compound queries that can't be expressed as a single filter.

## Efficient Query Design

**Always minimize the number of separate round-trips** to relays. Each query consumes relay capacity and may count against rate limits.

**✅ Efficient — single query with multiple kinds:**

```typescript
// Query repost variants in one request
const events = await nostr.query([{
  kinds: [1, 6, 16],
  '#e': [eventId],
  limit: 150,
}]);

// Separate by kind in JavaScript
const notes = events.filter((e) => e.kind === 1);
const reposts = events.filter((e) => e.kind === 6);
const genericReposts = events.filter((e) => e.kind === 16);
```

**❌ Inefficient — three separate round-trips:**

```typescript
const [notes, reposts, genericReposts] = await Promise.all([
  nostr.query([{ kinds: [1], '#e': [eventId] }]),
  nostr.query([{ kinds: [6], '#e': [eventId] }]),
  nostr.query([{ kinds: [16], '#e': [eventId] }]),
]);
```

### Optimization rules

1. **Combine kinds** into one filter: `kinds: [1, 6, 16]`.
2. **Use multiple filter objects** in a single `nostr.query()` call when different tag filters are needed simultaneously.
3. **Raise the `limit`** when combining kinds so you still receive enough of each type.
4. **Split by kind in JavaScript**, not by making separate requests.
5. **Respect relay capacity** — heavy parallel queries can trigger rate limits even when each individually would be fine.

## Event Validation

For kinds with required tags or strict schemas (most custom kinds, anything beyond kind 1), filter query results through a validator before returning them. Loose kinds (kind 1 text notes) rarely need validation — all tags are optional and `content` is freeform.

```typescript
import type { NostrEvent } from '@nostrify/nostrify';

// Example validator for NIP-52 calendar events
function validateCalendarEvent(event: NostrEvent): boolean {
  if (![31922, 31923].includes(event.kind)) return false;

  const d = event.tags.find(([n]) => n === 'd')?.[1];
  const title = event.tags.find(([n]) => n === 'title')?.[1];
  const start = event.tags.find(([n]) => n === 'start')?.[1];
  if (!d || !title || !start) return false;

  // Date-based events require YYYY-MM-DD
  if (event.kind === 31922 && !/^\d{4}-\d{2}-\d{2}$/.test(start)) return false;

  // Time-based events require a unix timestamp
  if (event.kind === 31923) {
    const ts = parseInt(start);
    if (isNaN(ts) || ts <= 0) return false;
  }

  return true;
}

function useCalendarEvents() {
  const { nostr } = useNostr();
  return useQuery({
    queryKey: ['calendar-events'],
    queryFn: async (c) => {
      const events = await nostr.query(
        [{ kinds: [31922, 31923], limit: 20 }],
        { signal: c.signal },
      );
      return events.filter(validateCalendarEvent);
    },
  });
}
```

Validation is a correctness layer, not a security layer. For trust-sensitive queries (admin actions, addressable events, moderator approvals), also constrain `authors` — see the `nostr-security` skill.

