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.
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
- Combine kinds into one filter:
kinds: [1, 6, 16]. - Use multiple filter objects in a single
nostr.query()call when different tag filters are needed simultaneously. - Raise the
limitwhen combining kinds so you still receive enough of each type. - Split by kind in JavaScript, not by making separate requests.
- 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.