Files
Alex Gleason bd68a32708 Split AGENTS.md into skills; compress to 358 lines
Extract eleven topic areas into loadable skills so AGENTS.md can serve
as a scannable overview instead of a specification dump. The file
shrinks from 1480 to 358 lines (~76%) while keeping every concrete
rule, critical code pattern, and pointer that an agent needs on first
read.

New Ditto-specific skills:
- nostr-kinds: NIP-vs-custom-kind decision framework, kind ranges,
  tag design, content-vs-tags, NIP.md update rule, and Ditto's
  seven-location UI registration checklist for new kinds (NoteCard,
  PostDetailPage, extraKinds.ts, KIND_LABELS/KIND_ICONS in
  CommentContext, WELL_KNOWN_KIND_LABELS in ExternalContentHeader,
  EmbeddedNote/EmbeddedNaddr, ReplyComposeModal).
- nostr-publishing: useNostrPublish, the read-modify-write pattern
  via fetchFreshEvent + prev for replaceable/addressable events,
  published_at contract, and d-tag collision prevention.
- nostr-queries: the standard useNostr + useQuery pattern,
  combining kinds into one filter to avoid rate limits, and the
  NIP-52 validator walkthrough.
- theming: @fontsource install flow, the Ditto runtime font-loader
  path (sanitizeUrl + sanitizeCssString), color scheme variables,
  useTheme toggle, and the isolate + negative-z-index gotcha.
- ci-cd-publishing: Zapstore NIP-46 bunker auth (zsp +
  nip46-auth.mjs), nsite deploys (nsyte nbunksec + configured
  relays/servers), and Google Play AAB uploads via fastlane supply
  (service-account JSON base64 encoding and rotation).
- capacitor-compat: WKWebView/WebView limitations, the
  downloadTextFile / openUrl helpers in src/lib/downloadFile.ts,
  platform detection, and the full plugin list.
- git-workflow: pre-commit validation order and the Regression-of:
  trailer convention used by the release skill's changelog
  generator.

Ported from mkstack, lightly adapted where needed:
- nip19-routing: root-level /:nip19 routing and filter construction
  patterns (adapted to reference Ditto's existing NIP19Page).
- nostr-relay-pools: nostr.relay() and nostr.group() for targeted
  queries.
- nostr-encryption: NIP-44 / NIP-04 via the user's signer.
- file-uploads: useUploadFile + Blossom + NIP-94 imeta tag
  construction.

AGENTS.md itself now follows mkstack's density — concrete rules inline,
one code example per section, pointer to the matching skill for details.
The enumerations that previously bloated it (every shadcn primitive,
every hook, every Capacitor plugin, the full NostrMetadata type dump,
the NIP-19 prefix reference table, etc.) are either removed in favor
of "ls the directory" or moved into their skill.
2026-04-26 23:13:30 -05:00

4.2 KiB

name, description
name description
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.

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:

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 NostrEvents. 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:

// 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:

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.

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.