Primal's cache server is a powerful extension layer built on top of the Nostr protocol. While standard NIP-01 relays provide basic event storage and filtering, Primal adds pre-computed analytics, trending algorithms, search capabilities, and rich social features that would be difficult or impossible to achieve efficiently with plain Nostr relays alone.
This document compares Primal's cache endpoints against what can be achieved using standard Nostr NIP-01 filters with basic relays. The goal is to understand:
- What Primal uniquely provides - Features requiring server-side computation, aggregation, or curation
- What standard Nostr can do - Core functionality achievable with NIP-01 filters across any compliant relay
- Where they overlap - Functionality that both can provide, with Primal offering convenience or performance benefits
Standard Nostr relays implement NIP-01, providing:
- Event storage and retrieval via WebSocket connections
- Filtering by event IDs, authors, kinds, tags, and timestamps
- Subscriptions for real-time event updates
- Simple publishing of signed events
What NIP-01 relays cannot do efficiently:
- Full-text search across event content
- Trending/scoring algorithms
- Analytics and aggregated statistics
- Content recommendations
- Media processing and CDN
- User relationship graphs (who follows whom)
- Reverse lookups (e.g., "who mentioned this event?")
Primal's cache server provides:
1. Pre-computed Analytics - Follower counts, engagement metrics, trending scores
2. Search - Full-text search across notes and user profiles
3. Discovery - Trending content, recommended users, popular hashtags
4. Performance - Cached aggregations instead of expensive client-side computation
5. Rich Metadata - Media thumbnails, link previews, user scores
6. Convenience - Single-request APIs for multi-step operations
7. Premium Features - Membership management, content backup, media uploads
Many Nostr queries follow predictable filter templates - parameterized patterns similar to SQL prepared statements. For example, to get reactions to an event:
{"kinds": [7], "#e": ["<event-id>"]}
Where all elements are fixed except <event-id>. This template-based thinking helps identify:
- Which queries can be standardized across clients
- Where Primal provides unique value beyond standard filters
- Opportunities for relay optimization
Legend:
- ✅ = Personalized functionality requiring realtime data
- ❌ = Has standard Nostr filter equivalent OR widely-accepted community standard
- 🟡 = Could be replicated by a bot publishing data as Nostr events on a schedule
Network & Statistics
- 🟡 net_stats
- 🟡 nostr_stats
- 🟡 server_name
Feed Endpoints
- ❌ feed (notes: "authored")
- ❌ feed (notes: "follows")
- ❌ feed (notes: "replies")
- ❌ feed (notes: "bookmarks")
- ✅ feed (notes: "user_media_thumbnails")
- ❌ feed_2
- ❌ thread_view
User Profile & Social Graph
- ❌ user_profile
- ❌ user_infos
- ❌ contact_list
- ❌ is_user_following
- ❌ user_followers
- ❌ mutual_follows
- ✅ user_profile_followed_by
Events & Actions
- ❌ events
- ❌ event_actions (all kinds)
Zaps
- ✅ zaps_feed
- ❌ user_zaps
- ✅ user_zaps_by_satszapped
- ✅ user_zaps_sent
- ✅ event_zaps_by_satszapped
- ✅ invoices_to_zap_receipts
Direct Messages
- ❌ get_directmsg_contacts
- ❌ get_directmsgs
- ✅ directmsg_count
- ✅ directmsg_count_2
- ✅ reset_directmsg_count
- ✅ reset_directmsg_counts
Content Moderation
- ❌ mutelist
- ❌ mutelists
- ✅ allowlist
- ❌ is_hidden_by_content_moderation
Explore & Discovery
- 🟡 explore_zaps
- 🟡 explore_people
- 🟡 explore_media
- 🟡 explore_topics
- 🟡 scored
- 🟡 scored_users
- 🟡 scored_users_24h
- 🟡 explore_global_trending_24h
- 🟡 explore_global_mostzapped_4h
- 🟡 explore
- ✅ explore_legend_counts
Search
- ❌ search
- ❌ advanced_search
- ❌ user_search
Long-form Content
- ❌ long_form_content_feed
- ❌ long_form_content_thread_view
- 🟡 get_recommended_reads
- 🟡 get_reads_topics
- 🟡 get_featured_authors
- ✅ articles_stats
- ❌ drafts
- ✅ top_article
Bookmarks & Highlights
- ❌ get_bookmarks
- ❌ get_highlights
Relays
- ❌ get_user_relays
- ❌ get_user_relays_2
- 🟡 relays
- 🟡 get_default_relays
Recommendations
- 🟡 get_recommended_users
- 🟡 get_suggested_users
Feeds & Directives
- ✅ feed_directive
- ✅ feed_directive_2
- ✅ mega_feed_directive
- ✅ advanced_feed
- 🟡 get_advanced_feeds
- 🟡 get_home_feeds
- 🟡 get_reads_feeds
- ✅ enrich_feed_events
DVM
- 🟡 get_dvm_feeds
- ✅ dvm_feed_info
- 🟡 get_featured_dvm_feeds
Settings & Configuration
- ❌ set_app_settings
- ❌ get_app_settings
- ❌ get_app_settings_2
- 🟡 get_default_app_settings
- ❌ set_app_subsettings
- ❌ get_app_subsettings
- 🟡 get_default_app_subsettings
- 🟡 client_config
- 🟡 get_app_releases
Notifications
- ❌ notifications
- ✅ notification_counts
- ✅ notification_counts_2
- ❌ get_notifications
- ✅ set_notifications_seen
- ✅ get_notifications_seen
Push Notifications
- ✅ update_push_notification_token
- ✅ update_push_notification_token_for_nip46
- ❌ events_nip46
Hashtags & Trending
- 🟡 trending_hashtags_4h
- 🟡 trending_hashtags_7d
- 🟡 trending_images
- 🟡 trending_images_4h
Lists & Collections
- ❌ parameterized_replaceable_list
- ❌ parametrized_replaceable_event
- ❌ parametrized_replaceable_events
- ❌ replaceable_event
- ❌ follow_lists
- ❌ follow_list
Content Filtering
- ✅ search_filterlist
- ✅ get_filterlist
- ✅ check_filterlist
Reporting
- ❌ report_user
- ❌ report_note
Event Broadcasting
- ❌ import_events
- ❌ broadcast_reply
- ❌ broadcast_events
Membership Features
- ✅ membership_media_management_stats
- ✅ membership_media_management_uploads
- ✅ membership_media_management_delete
- ❌ membership_recovery_contact_lists
- ❌ membership_recover_contact_list
- ❌ membership_content_stats
- ❌ membership_content_backup
- ✅ membership_content_rebroadcast_start
- ✅ membership_content_rebroadcast_cancel
- ✅ membership_content_rebroadcast_status
- ✅ rebroadcasting_status
- 🟡 membership_legends_leaderboard
- 🟡 membership_premium_leaderboard
- ❌ creator_paid_tiers
Media Management
- ❌ upload
- ❌ upload_chunk
- ❌ upload_complete
- ❌ upload_cancel
- ❌ get_media_metadata
- ❌ get_recommended_blossom_servers
Utilities
- ✅ user_of_ln_address
- ❌ nip19_decode
- ✅ set_last_time_user_was_online
- ✅ trusted_users
- ❌ note_mentions
- ✅ note_mentions_count
- ❌ find_reposts
- ✅ user_profile_scored_content
- ✅ user_profile_scored_media_thumbnails
---
The following table maps Primal cache endpoints to their equivalent standard Nostr filters (where applicable). Some endpoints provide pre-computed analytics or complex aggregations that cannot be replicated with a single standard filter.
net_stats - Network statistics
No equivalent standard filter - aggregates global statistics
nostr_stats - Nostr network stats
No equivalent standard filter - aggregates network-wide metrics
server_name - Get server name
No equivalent standard filter - server metadata
feed (notes: "authored") - User's posts
{"kinds": [1], "authors": ["<pubkey>"], "limit": <n>}
feed (notes: "follows") - Following feed
// Step 1: Get contact list
{"kinds": [3], "authors": ["<pubkey>"]}
// Step 2: Get posts from followed users (extract pubkeys from step 1 tags)
{"kinds": [1], "authors": ["<followed_pubkey1>", "<followed_pubkey2>", ...], "limit": <n>}
feed (notes: "replies") - User's replies
{"kinds": [1], "authors": ["<pubkey>"], "#e": [""], "limit": <n>}
feed (notes: "bookmarks") - Bookmarked posts
// Step 1: Get bookmark list
{"kinds": [10003], "authors": ["<pubkey>"]}
// Or for categorized bookmarks: {"kinds": [30003], "authors": ["<pubkey>"]}
// Step 2: Get bookmarked events (extract event IDs from step 1 tags)
{"ids": ["<bookmarked_id1>", "<bookmarked_id2>", ...]}
feed (notes: "user_media_thumbnails") - Posts with media
feed_2 - Extended feed endpoint
feed, see above variants
thread_view - Conversation thread
// Step 1: Get the target event
{"ids": ["<event_id>"]}
// Step 2: Get replies to the event
{"kinds": [1], "#e": ["<event_id>"], "limit": <n>}
// Step 3: Get parent events (parse "e" tags from step 1, recursively fetch ancestors)
{"ids": ["<parent_id1>", "<parent_id2>", ...]}
user_profile - User profile information
{"kinds": [0], "authors": ["<pubkey>"]}
user_infos - Batch user information
{"kinds": [0], "authors": ["<pubkey1>", "<pubkey2>", ...]}
contact_list - User's contact list
{"kinds": [3], "authors": ["<pubkey>"]}
is_user_following - Check if user follows another
// Step 1: Get follower's contact list
{"kinds": [3], "authors": ["<follower_pubkey>"]}
// Step 2: Parse tags to check if target_pubkey is in the "p" tags
user_followers - Get user followers
{"kinds": [3], "#p": ["<pubkey>"]}
mutual_follows - Mutual followers between users
// Step 1: Get user A's contact list
{"kinds": [3], "authors": ["<pubkey_a>"]}
// Step 2: Get user B's contact list
{"kinds": [3], "authors": ["<pubkey_b>"]}
// Step 3: Compare "p" tags to find mutual follows
user_profile_followed_by - Who follows this user
events - Get specific events
{"ids": ["<event_id1>", "<event_id2>", ...]}
event_actions (kind: 7) - Event reactions
{"kinds": [7], "#e": ["<event_id>"], "limit": <n>}
event_actions (kind: 1) - Event replies
{"kinds": [1], "#e": ["<event_id>"], "limit": <n>}
event_actions (kind: 6) - Event reposts
{"kinds": [6], "#e": ["<event_id>"], "limit": <n>}
event_actions (kind: 9735) - Event zaps
{"kinds": [9735], "#e": ["<event_id>"], "limit": <n>}
zaps_feed - Zap feed
user_zaps - Zaps received by user
{"kinds": [9735], "#p": ["<pubkey>"], "limit": <n>}
user_zaps_by_satszapped - Zaps sorted by amount
user_zaps_sent - Zaps sent by user
event_zaps_by_satszapped - Zaps on event sorted by amount
invoices_to_zap_receipts - Convert invoices to zap receipts
get_directmsg_contacts - Get DM contacts
// Step 1: Get DMs where user is receiver
{"kinds": [4], "#p": ["<user_pubkey>"], "limit": <n>}
// Step 2: Get DMs where user is sender
{"kinds": [4], "authors": ["<user_pubkey>"], "limit": <n>}
// Step 3: Extract unique pubkeys from authors and "p" tags
get_directmsgs - Get direct messages
// Step 1: Get messages from sender to receiver
{"kinds": [4], "authors": ["<sender>"], "#p": ["<receiver>"], "limit": <n>}
// Step 2: Get messages from receiver to sender
{"kinds": [4], "authors": ["<receiver>"], "#p": ["<sender>"], "limit": <n>}
// Step 3: Merge and sort by created_at for conversation view
directmsg_count - DM count
directmsg_count_2 - Extended DM count
reset_directmsg_count - Reset DM counter
reset_directmsg_counts - Reset all DM counters
mutelist - Get mute list
{"kinds": [10000], "authors": ["<pubkey>"]}
mutelists - Get multiple mute lists
{"kinds": [10000], "authors": ["<pubkey1>", "<pubkey2>", ...]}
allowlist - Get allow list
is_hidden_by_content_moderation - Check if content is hidden
// Step 1: Get user's mute list
{"kinds": [10000], "authors": ["<viewer_pubkey>"]}
// Step 2: Check if event author or event ID is in mute list tags
explore_zaps - Trending zaps
explore_people - Discover users
explore_media - Trending media
explore_topics - Trending topics
scored - Get scored/trending content
scored_users - Trending users
scored_users_24h - Trending users (24h)
explore_global_trending_24h - Global trending (24h)
explore_global_mostzapped_4h - Most zapped globally (4h)
explore - General explore endpoint
explore_legend_counts - Network statistics for user
search - Search events
{"kinds": [1], "search": "<query>", "limit": <n>}
advanced_search - Advanced search with filters
{"kinds": [<kind_filter>], "search": "<query>", "limit": <n>}
user_search - Search users
{"kinds": [0], "search": "<query>", "limit": <n>}
long_form_content_feed - Article feed
{"kinds": [30023], "authors": ["<pubkey>"], "limit": <n>}
long_form_content_thread_view - Article with comments
// Step 1: Get the article
{"ids": ["<article_event_id>"]}
// Step 2: Get comments on the article (using "a" tag with kind:pubkey:d-tag format)
{"kinds": [1], "#a": ["30023:<pubkey>:<d_tag>"], "limit": <n>}
get_recommended_reads - Recommended articles
get_reads_topics - Article topics
get_featured_authors - Featured article authors
articles_stats - Article statistics
drafts - Article drafts
{"kinds": [31234], "#k": ["30023"], "authors": ["<pubkey>"]}
top_article - Top article
get_bookmarks - Get bookmarks
{"kinds": [10003], "authors": ["<pubkey>"]}
get_highlights - Get highlights
{"kinds": [9802], "authors": ["<pubkey>"]}
get_user_relays - User relay list
{"kinds": [10002], "authors": ["<pubkey>"]}
get_user_relays_2 - Extended relay list
relays - Popular relays
get_default_relays - Default relay recommendations
get_recommended_users - Recommended users to follow
get_suggested_users - Suggested user accounts
feed_directive - Custom feed directive
feed_directive_2 - Extended feed directive
mega_feed_directive - Mega feed endpoint
advanced_feed - Advanced feed
get_advanced_feeds - Available advanced feeds
get_home_feeds - Home feed configurations
get_reads_feeds - Article feed configurations
enrich_feed_events - Enrich feed with metadata
get_dvm_feeds - DVM feed list
dvm_feed_info - DVM feed details
get_featured_dvm_feeds - Featured DVM feeds
set_app_settings - Set user app settings
get_app_settings - Get app settings
{"kinds": [30078], "authors": ["<pubkey>"], "#d": ["primal_app_settings"]}
get_app_settings_2 - Extended app settings
get_default_app_settings - Default app settings
set_app_subsettings - Set app subsettings
get_app_subsettings - Get app subsettings
get_default_app_subsettings - Default subsettings
client_config - Client configuration
get_app_releases - App release information
notifications - Get notifications
// Step 1: Get mentions
{"kinds": [1], "#p": ["<pubkey>"], "limit": <n>}
// Step 2: Get reactions to user's posts
{"kinds": [7], "#p": ["<pubkey>"], "limit": <n>}
// Step 3: Get zaps
{"kinds": [9735], "#p": ["<pubkey>"], "limit": <n>}
// Step 4: Get reposts (requires first getting user's posts, then finding kind 6 events)
// This is complex - need user's post IDs first
// Step 5: Merge, deduplicate, and sort by created_at
notification_counts - Notification counts
notification_counts_2 - Extended notification counts
get_notifications - Get notifications with filters
notifications but with additional filtering by notification type
set_notifications_seen - Mark notifications as seen
get_notifications_seen - Get seen notification timestamp
update_push_notification_token - Update push token
update_push_notification_token_for_nip46 - Update push token for NIP-46
events_nip46 - NIP-46 events
{"kinds": [24133], "#p": ["<pubkey>"]}
trending_hashtags_4h - Trending hashtags (4h)
trending_hashtags_7d - Trending hashtags (7d)
trending_images - Trending images
trending_images_4h - Trending images (4h)
parameterized_replaceable_list - Get parameterized replaceable list
{"kinds": [<kind>], "authors": ["<pubkey>"], "#d": ["<identifier>"]}
parametrized_replaceable_event - Get single parameterized event
{"kinds": [<kind>], "authors": ["<pubkey>"], "#d": ["<identifier>"]}
parametrized_replaceable_events - Get multiple parameterized events
{"kinds": [<kind>], "authors": ["<pubkey>"], "#d": ["<id1>", "<id2>", ...]}
replaceable_event - Get replaceable event
{"kinds": [<kind>], "authors": ["<pubkey>"]}
follow_lists - Get follow lists
{"kinds": [30000], "authors": ["<pubkey>"]}
follow_list - Get specific follow list
{"kinds": [30000], "authors": ["<pubkey>"], "#d": ["<identifier>"]}
search_filterlist - Search filter list
get_filterlist - Get filter list
check_filterlist - Check if filtered
report_user - Report user
report_note - Report note
import_events - Import events
broadcast_reply - Broadcast reply
broadcast_events - Broadcast multiple events
membership_media_management_stats - Media storage stats
membership_media_management_uploads - List uploads
membership_media_management_delete - Delete uploads
membership_recovery_contact_lists - Recover contact lists
// Query kind 3 events, one per day (keeping the one with most follows per day)
{"kinds": [3], "authors": ["<pubkey>"], "limit": 30}
membership_recover_contact_list - Recover specific contact list
{"ids": ["<event_id>"]}
membership_content_stats - Content backup stats
membership_content_backup - Backup content
{"kinds": [<kind_filter>], "authors": ["<pubkey>"], "since": <timestamp>, "limit": <n>}
membership_content_rebroadcast_start - Start rebroadcast
membership_content_rebroadcast_cancel - Cancel rebroadcast
membership_content_rebroadcast_status - Rebroadcast status
rebroadcasting_status - Get rebroadcast status
membership_legends_leaderboard - Legends leaderboard
membership_premium_leaderboard - Premium leaderboard
creator_paid_tiers - Creator subscription tiers
upload - Upload media
upload_chunk - Upload media chunk
upload_complete - Complete chunked upload
upload_cancel - Cancel upload
get_media_metadata - Get media metadata
get_recommended_blossom_servers - Blossom server recommendations
user_of_ln_address - Get pubkey from Lightning address
nip19_decode - Decode NIP-19 identifiers
set_last_time_user_was_online - Update online status
trusted_users - Get trusted users
note_mentions - Get note mentions
{"kinds": [1], "#e": ["<event_id>"]}
note_mentions_count - Count note mentions
find_reposts - Find reposts of event
{"kinds": [6], "#e": ["<event_id>"]}
user_profile_scored_content - User's top content
user_profile_scored_media_thumbnails - User's top media
Some templates have natural variations that maintain the same semantic purpose:
{
"kinds": [7],
"#e": ["<event-id>"],
"authors": ["<pubkey>"]
}
This is a refinement of the basic reaction template, filtering for a specific user's reaction to an event.
{
"kinds": [1, 6],
"authors": ["<pubkey>"]
}
Fetching both regular posts (kind 1) and reposts (kind 6) from an author.
Some filter constructions, while valid, don't follow clean template patterns:
{
"ids": ["<event-id>"],
"authors": ["<pubkey>"],
"kinds": [1]
}
When querying by ids, other filters are typically redundant (event IDs are globally unique).
{
"kinds": [1]
}
Without limit, authors, or time bounds, this requests the entire history of kind 1 events - likely to be rejected by relays.
When building Nostr applications:
1. Define filter factories for each template your app uses
2. Name your templates semantically (e.g., getUserProfile, getEventReactions)
3. Document parameters clearly for each template
4. Validate inputs before substitution to prevent malformed filters
5. Consider limits - always include sensible limit values for potentially large result sets
const FilterTemplates = {
userProfile: (pubkey: string) => ({
kinds: [0],
authors: [pubkey]
}),
eventReactions: (eventId: string, limit?: number) => ({
kinds: [7],
"#e": [eventId],
...(limit && { limit })
}),
followingFeed: (pubkeys: string[], limit: number = 20) => ({
kinds: [1],
authors: pubkeys,
limit
})
};
By thinking of Nostr filters as templates rather than arbitrary JSON structures, we gain clarity in our application design, improve performance through predictable patterns, and build more maintainable codebases. This mental model bridges the gap between Nostr's flexible filter system and the structured query approaches developers are familiar with from traditional databases.