mega_feed_directive Cache EndpointThe mega_feed_directive is Primal's primary and most versatile feed endpoint. It accepts a JSON-encoded specification string that determines what type of feed to return. This single endpoint replaces the need for many specialized feed endpoints.
["REQ", "subscription_id", {
"cache": ["mega_feed_directive", {
"spec": "<json_specification>",
"limit": 20,
"offset": 0,
"since": 0,
"until": 0,
"user_pubkey": "<optional_viewer_pubkey>"
}]
}]
- spec (required): JSON-encoded string specifying the feed type and parameters
- limit (optional): Maximum number of items to return (default varies by spec type)
- offset (optional): Pagination offset
- since (optional): Unix timestamp for time-based pagination
- until (optional): Unix timestamp for time-based pagination
- user_pubkey (optional): Viewer's pubkey for personalization
spec FieldThe spec field is a JSON-encoded string (not a JSON object) that contains the feed specification. Each spec has an id field that determines the feed type, plus additional parameters specific to that type.
{
"id": "<feed_type>",
"kind": "notes" | "reads", // Content type
// ... type-specific parameters
}
---
#### Advanced Search (advsearch)
Search for content across Nostr. This is what Primal's web app uses for the search feature.
Spec Format:
{
"id": "advsearch",
"query": "<search_query>"
}
Example:
const spec = JSON.stringify({
id: "advsearch",
query: "bitcoin"
});
const response = await server.megaFeedDirective(spec, { limit: 20 });
Example Request:
["REQ", "search_123", {
"cache": ["mega_feed_directive", {
"spec": "{\"id\":\"advsearch\",\"query\":\"bitcoin\"}",
"limit": 20,
"user_pubkey": "0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd"
}]
}]
User Search (Kind 0):
{
"id": "advsearch",
"query": "kind:0 alex"
}
---
#### User's Notes Feed (feed)
Get notes from a specific user.
Spec Format:
{
"id": "feed",
"kind": "notes",
"pubkey": "<user_pubkey>"
}
Example:
const spec = JSON.stringify({
id: "feed",
kind: "notes",
pubkey: "0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd"
});
const response = await server.megaFeedDirective(spec, { limit: 20 });
---
#### Latest Notes (latest)
Latest notes from users the viewer follows.
Spec Format:
{
"id": "latest",
"kind": "notes"
}
With Replies:
{
"id": "latest",
"kind": "notes",
"include_replies": true
}
Example:
const spec = JSON.stringify({
id: "latest",
kind: "notes",
include_replies: true
});
---
#### Global Trending (global-trending)
Globally trending notes across various time windows.
Spec Format:
{
"id": "global-trending",
"kind": "notes",
"hours": 1 | 4 | 12 | 24 | 48 | 168 // 1h, 4h, 12h, 24h, 48h, 7d
}
Examples:
24-hour trending:
const spec = JSON.stringify({
id: "global-trending",
kind: "notes",
hours: 24
});
Weekly trending:
const spec = JSON.stringify({
id: "global-trending",
kind: "notes",
hours: 168 // 7 days = 168 hours
});
Example Request:
["REQ", "trending_24h", {
"cache": ["mega_feed_directive", {
"spec": "{\"id\":\"global-trending\",\"kind\":\"notes\",\"hours\":24}",
"limit": 20
}]
}]
---
#### All Notes (Firehose) (all-notes)
Latest global notes from across the network. Can be very high volume.
Spec Format:
{
"id": "all-notes",
"kind": "notes"
}
Example:
const spec = JSON.stringify({
id: "all-notes",
kind: "notes"
});
// Use with caution - this is a firehose!
const response = await server.megaFeedDirective(spec, { limit: 50 });
---
#### Latest Articles (latest)
Same as notes, but for long-form content (kind 30023).
Spec Format:
{
"id": "latest",
"kind": "reads"
}
Example:
const spec = JSON.stringify({
id: "latest",
kind: "reads"
});
---
#### Trending Articles (global-trending)
Trending long-form articles.
Spec Format:
{
"id": "global-trending",
"kind": "reads",
"hours": 24 | 48 | 168
}
---
#### Topic Feed
Notes filtered by a specific hashtag/topic.
Spec Format:
{
"id": "topic",
"kind": "notes",
"topic": "<hashtag_without_#>"
}
Example:
const spec = JSON.stringify({
id: "topic",
kind: "notes",
topic: "bitcoin"
});
---
#### User Drafts (drafts)
User's draft posts (kind 31234).
Note: Drafts use a different cache endpoint (drafts), not mega_feed_directive, but are mentioned here for completeness.
---
The response contains events of various kinds providing feed data and metadata:
| Kind | Purpose |
|------|---------|
| 0 | User metadata |
| 1 | Note (text post) |
| 6 | Repost |
| 9735 | Zap receipt |
| 30023 | Long-form article |
| 31234 | Draft |
| 10000100 | Feed range (pagination info) |
| 10000107 | Note statistics |
| 10000113 | Note actions |
| 10000118 | User statistics |
Provides pagination information for the feed:
{
"kind": 10000100,
"content": "{\"since\":1738976703,\"until\":1739062799,\"order_by\":\"created_at\",\"elements\":[\"note_id_1\",\"note_id_2\",...]}",
...
}
The elements array provides the order of items in the feed.
---
Many feeds support a user_pubkey parameter to personalize results based on the viewer's follows and preferences:
["REQ", "feed_123", {
"cache": ["mega_feed_directive", {
"spec": "{\"id\":\"latest\",\"kind\":\"notes\"}",
"limit": 20,
"user_pubkey": "viewer_pubkey_here"
}]
}]
Use until/since with the timestamps from the feed range event:
// First page
const page1 = await server.megaFeedDirective(spec, { limit: 20 });
// Get 'until' from feed range event
const until = page1.events.find(e => e.kind === 10000100);
const range = JSON.parse(until.content);
// Next page
const page2 = await server.megaFeedDirective(spec, {
limit: 20,
until: range.until,
offset: 20
});
Use the kind field in the spec to filter between:
- "notes" - Short-form posts (kind 1)
- "reads" - Long-form articles (kind 30023)
---
const searchSpec = JSON.stringify({
id: "advsearch",
query: "nostr protocol"
});
let offset = 0;
const limit = 20;
// Get first page
const page1 = await server.megaFeedDirective(searchSpec, {
limit,
offset,
userPubkey: viewerPubkey
});
console.log(Found ${page1.posts.length} posts);
// Get second page
offset += limit;
const page2 = await server.megaFeedDirective(searchSpec, {
limit,
offset,
userPubkey: viewerPubkey
});
// Get 4-hour trending
const trendingSpec = JSON.stringify({
id: "global-trending",
kind: "notes",
hours: 4
});
const trending = await server.megaFeedDirective(trendingSpec, {
limit: 50
});
for (const post of trending.posts) {
console.log(${post.event.content} (score: ${post.stats?.score}));
}
const userSpec = JSON.stringify({
id: "feed",
kind: "notes",
pubkey: "0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd"
});
const userPosts = await server.megaFeedDirective(userSpec, {
limit: 20,
userPubkey: viewerPubkey // For personalized stats
});
---
Primal provides endpoints to discover available feed configurations:
["REQ", "home_feeds", {
"cache": ["get_home_feeds"]
}]
Returns a list of feed configurations with their specs.
["REQ", "reads_feeds", {
"cache": ["get_reads_feeds"]
}]
---
1. Spec is a String: The spec field must be a JSON-encoded string, not a raw JSON object
2. Feed-Specific Limits: Default limits vary by feed type (check server response)
3. Authentication: Some feeds (like latest) require user_pubkey to work properly
4. Rate Limiting: Firehose feeds (all-notes) may have stricter rate limits
5. Backwards Compatibility: Older feed types may still be supported but consider using mega_feed_directive equivalents
---
- Primal Server API Documentation
- PrimalServer TypeScript Client
- Web App Feed Implementations: repos/primal-web-app/src/lib/feed.ts