NIP-XX: window.nostr.content capability for web browsers
draft | optional | extension of NIP-07
Abstract
This NIP defines a standard interface for browser extensions to expose content-level mute lists to web applications via window.nostr.content. It extends the signer interface defined in NIP-07 and is designed to complement the Web of Trust interface (window.nostr.wot).
While WoT filtering operates on author identity, this NIP operates on content signals — hashtags, keywords, content warnings, languages, and event kinds — independent of who posted them.
Motivation
Nostr clients running in web browsers need to filter content based on user-defined rules beyond author trust. Today each client must implement its own mute logic, maintain its own lists, and apply its own matching — duplicating work across every client the user touches.
By exposing a common content mute API on window.nostr, any Nostr web client can apply the user's content preferences without reimplementing them. The extension becomes a single source of truth for what the user does not want to see.
Relation to Existing NIPs
- NIP-07: This NIP extends
window.nostr with a content sub-object. The two capabilities are independent and composable.
- NIP-51: Mute lists (kind:10000) defined in NIP-51 include pubkeys and event IDs. This NIP focuses on the content-signal subset of mute lists (hashtags, keywords, content warnings, languages, kinds) and does not duplicate pubkey filtering, which belongs to WoT.
window.nostr.wot: Identity filtering (who) lives in WoT. Content filtering (what) lives here. Clients SHOULD apply both layers.
Detection
Extensions MUST inject window.nostr.content before or at document_start. Clients MUST use the following detection pattern:
function onContentReady(callback) {
if (window.nostr?.content) {
callback();
} else {
document.addEventListener('nostr-content-ready', callback, { once: true });
}
}
Extensions MUST dispatch a nostr-content-ready event on document once the API object is available.
Data Model
A content mute list contains zero or more entries across these signal types:
| Signal Type |
Description |
Example |
hashtags |
Lowercase tag values from t tags |
"bitcoin", "ai" |
keywords |
Substring or phrase matches against event content |
"giveaway", "OnlyFans" |
contentWarnings |
Values of content-warning tags |
"nudity", "violence" |
languages |
Values of l tags (BCP-47 codes) |
"zh", "es" |
kinds |
Nostr event kind numbers |
30023, 1063 |
events |
Specific event IDs to mute |
64-char hex event ID |
threads |
Root event IDs — mutes the event and all replies |
64-char hex event ID |
Matching for hashtags, contentWarnings, and languages MUST be case-insensitive. Matching for keywords SHOULD be case-insensitive. Implementations MAY support glob or regex patterns for keywords but MUST document this behavior.
API
All methods return Promises. Event objects passed to this API MUST conform to the standard Nostr event structure.
getMuteList(): Promise<MuteList>
Returns the user's full content mute list.
const list = await window.nostr.content.getMuteList();
// {
// hashtags: ["ai", "nft"],
// keywords: ["giveaway", "click here"],
// contentWarnings: ["nudity"],
// languages: ["zh"],
// kinds: [30023],
// events: ["a1b2c3..."],
// threads: ["d4e5f6..."]
// }
Return type:
interface MuteList {
hashtags: string[];
keywords: string[];
contentWarnings: string[];
languages: string[];
kinds: number[];
events: string[]; // specific event IDs (hex)
threads: string[]; // root event IDs (hex) — mutes entire thread
}
All fields MUST be present. Fields with no entries MUST return empty arrays, not null.
isMuted(event: NostrEvent): Promise<MuteResult>
Evaluates a single event against the user's mute list. Returns a result object indicating whether the event is muted and, if so, why.
const result = await window.nostr.content.isMuted(event);
// { muted: true, reason: "hashtag", matched: "ai" }
// { muted: false }
Return type:
interface MuteResult {
muted: boolean;
reason?: 'hashtag' | 'keyword' | 'contentWarning' | 'language' | 'kind' | 'event' | 'thread';
matched?: string | number;
}
reason and matched MUST be present when muted is true.
matched contains the specific value that triggered the mute (the hashtag string, keyword string, kind number, event ID, or thread root ID).
- If multiple signals match, implementations SHOULD return the first match in the order:
event → thread → kind → hashtag → contentWarning → language → keyword.
filterMuted(events: NostrEvent[]): Promise<NostrEvent[]>
Returns the subset of events that are not muted. Muted events are excluded from the result. The order of non-muted events MUST be preserved.
const visible = await window.nostr.content.filterMuted(feedEvents);
This is the primary method for feed-level filtering. Clients SHOULD prefer this over calling isMuted in a loop.
getStatus(): Promise<ContentStatus>
Returns the readiness state of the content module.
const status = await window.nostr.content.getStatus();
// { configured: true, muteCount: 12 }
Return type:
interface ContentStatus {
configured: boolean;
muteCount: number; // total number of mute entries across all signal types
}
Matching Rules
Implementations MUST apply the following matching logic when evaluating an event:
event: Muted if event.id is in events.
thread: Muted if the event is part of a muted thread. An event is part of a muted thread if:
- Its
id matches a thread root in threads, OR
- It has an
e tag with marker "root" whose value is in threads, OR
- It has an
e tag with marker "reply" whose value is in threads (for clients that do not set root markers), OR
- It has an
A or a tag referencing a muted thread root (for parameterized replaceable events)
Implementations SHOULD resolve thread membership transitively: if event B is a reply to muted root A, and event C is a reply to B, C MUST also be muted. Clients passing events to filterMuted SHOULD include enough tag context for the extension to determine thread membership. Implementations MAY request that clients pass a rootId hint if thread context cannot be determined from tags alone.
Open question: Should threads accept both event IDs (hex, for kind:1 threads) and NIP-19 naddr coordinates (for kind:30023 long-form articles and other parameterized replaceable events whose replies reference them via a tags)? Restricting to hex event IDs keeps the API uniform but excludes a common thread root type. Accepting coordinates adds complexity but covers the full range of Nostr thread patterns.
kind: Muted if event.kind is in kinds.
hashtag: Muted if any value in the event's t tags matches a hashtag in the mute list (case-insensitive).
contentWarning: Muted if any content-warning tag value matches an entry in contentWarnings (case-insensitive). Events with a content-warning tag present but with an empty value SHOULD be muted if "" is in the contentWarnings list.
language: Muted if any l tag value matches an entry in languages (case-insensitive).
keyword: Muted if the event's content field contains any keyword as a substring (case-insensitive). Implementations MAY also match against tag values.
Security Considerations
- Read-only API. Web pages MUST NOT be able to modify the mute list through this API.
- List privacy. The mute list reveals user preferences. Implementations MAY require the page to have prior user consent (e.g., via NIP-07
getPublicKey approval) before returning list contents via getMuteList. isMuted and filterMuted MAY be available without consent since they return less information than the raw list.
- Event and thread ID privacy. Muted event IDs and thread root IDs may reveal which specific content a user found objectionable. Implementations SHOULD apply the same consent gate to
events and threads in getMuteList as to other list fields. isMuted and filterMuted MUST still function correctly without exposing raw ID lists to the page.
- No identity signals. This API MUST NOT expose pubkeys. Identity filtering belongs to WoT. If a client passes an event to
isMuted, the extension MUST NOT use the event's pubkey field in its evaluation.
- Performance. Implementations SHOULD cache compiled keyword matchers and MUST NOT perform synchronous I/O during evaluation.
Error Handling
All methods MUST reject with an Error if called with malformed input (e.g., invalid event structure). Implementations MUST NOT reject when an event simply does not match — return { muted: false } instead.
Rate Limiting
Implementations SHOULD enforce per-method rate limits to prevent abuse by malicious pages. Rate-limited requests MUST reject with a descriptive error (e.g., "Rate limited").
Example Usage
function onContentReady(callback) {
if (window.nostr?.content) callback();
else document.addEventListener('nostr-content-ready', callback, { once: true });
}
onContentReady(async () => {
const { configured } = await window.nostr.content.getStatus();
if (!configured) return;
// Filter a feed in one call
const visible = await window.nostr.content.filterMuted(feedEvents);
// Check a single event with reason
const result = await window.nostr.content.isMuted(event);
if (result.muted) {
console.log(`Muted: ${result.reason} → "${result.matched}"`);
}
// Combine with WoT for full filtering
const authors = visible.map(e => e.pubkey);
const trustedAuthors = new Set(await window.nostr.wot.filterByWoT(authors, 2));
const feed = visible.filter(e => trustedAuthors.has(e.pubkey));
});
Reference Implementation
Authors
- npub1gxdhmu9swqduwhr6zptjy4ya693zp3ql28nemy4hd97kuufyrqdqwe5zfk
NIP-XX: window.nostr.content capability for web browsers
draft|optional|extension of NIP-07Abstract
This NIP defines a standard interface for browser extensions to expose content-level mute lists to web applications via
window.nostr.content. It extends the signer interface defined in NIP-07 and is designed to complement the Web of Trust interface (window.nostr.wot).While WoT filtering operates on author identity, this NIP operates on content signals — hashtags, keywords, content warnings, languages, and event kinds — independent of who posted them.
Motivation
Nostr clients running in web browsers need to filter content based on user-defined rules beyond author trust. Today each client must implement its own mute logic, maintain its own lists, and apply its own matching — duplicating work across every client the user touches.
By exposing a common content mute API on
window.nostr, any Nostr web client can apply the user's content preferences without reimplementing them. The extension becomes a single source of truth for what the user does not want to see.Relation to Existing NIPs
window.nostrwith acontentsub-object. The two capabilities are independent and composable.window.nostr.wot: Identity filtering (who) lives in WoT. Content filtering (what) lives here. Clients SHOULD apply both layers.Detection
Extensions MUST inject
window.nostr.contentbefore or atdocument_start. Clients MUST use the following detection pattern:Extensions MUST dispatch a
nostr-content-readyevent ondocumentonce the API object is available.Data Model
A content mute list contains zero or more entries across these signal types:
hashtagsttags"bitcoin","ai"keywordscontent"giveaway","OnlyFans"contentWarningscontent-warningtags"nudity","violence"languagesltags (BCP-47 codes)"zh","es"kinds30023,1063eventsthreadsMatching for
hashtags,contentWarnings, andlanguagesMUST be case-insensitive. Matching forkeywordsSHOULD be case-insensitive. Implementations MAY support glob or regex patterns for keywords but MUST document this behavior.API
All methods return Promises. Event objects passed to this API MUST conform to the standard Nostr event structure.
getMuteList(): Promise<MuteList>Returns the user's full content mute list.
Return type:
All fields MUST be present. Fields with no entries MUST return empty arrays, not
null.isMuted(event: NostrEvent): Promise<MuteResult>Evaluates a single event against the user's mute list. Returns a result object indicating whether the event is muted and, if so, why.
Return type:
reasonandmatchedMUST be present whenmutedistrue.matchedcontains the specific value that triggered the mute (the hashtag string, keyword string, kind number, event ID, or thread root ID).event→thread→kind→hashtag→contentWarning→language→keyword.filterMuted(events: NostrEvent[]): Promise<NostrEvent[]>Returns the subset of
eventsthat are not muted. Muted events are excluded from the result. The order of non-muted events MUST be preserved.This is the primary method for feed-level filtering. Clients SHOULD prefer this over calling
isMutedin a loop.getStatus(): Promise<ContentStatus>Returns the readiness state of the content module.
Return type:
Matching Rules
Implementations MUST apply the following matching logic when evaluating an event:
event: Muted if
event.idis inevents.thread: Muted if the event is part of a muted thread. An event is part of a muted thread if:
idmatches a thread root inthreads, ORetag with marker"root"whose value is inthreads, ORetag with marker"reply"whose value is inthreads(for clients that do not set root markers), ORAoratag referencing a muted thread root (for parameterized replaceable events)Implementations SHOULD resolve thread membership transitively: if event B is a reply to muted root A, and event C is a reply to B, C MUST also be muted. Clients passing events to
filterMutedSHOULD include enough tag context for the extension to determine thread membership. Implementations MAY request that clients pass arootIdhint if thread context cannot be determined from tags alone.kind: Muted if
event.kindis inkinds.hashtag: Muted if any value in the event's
ttags matches a hashtag in the mute list (case-insensitive).contentWarning: Muted if any
content-warningtag value matches an entry incontentWarnings(case-insensitive). Events with acontent-warningtag present but with an empty value SHOULD be muted if""is in thecontentWarningslist.language: Muted if any
ltag value matches an entry inlanguages(case-insensitive).keyword: Muted if the event's
contentfield contains any keyword as a substring (case-insensitive). Implementations MAY also match against tag values.Security Considerations
getPublicKeyapproval) before returning list contents viagetMuteList.isMutedandfilterMutedMAY be available without consent since they return less information than the raw list.eventsandthreadsingetMuteListas to other list fields.isMutedandfilterMutedMUST still function correctly without exposing raw ID lists to the page.isMuted, the extension MUST NOT use the event'spubkeyfield in its evaluation.Error Handling
All methods MUST reject with an
Errorif called with malformed input (e.g., invalid event structure). Implementations MUST NOT reject when an event simply does not match — return{ muted: false }instead.Rate Limiting
Implementations SHOULD enforce per-method rate limits to prevent abuse by malicious pages. Rate-limited requests MUST reject with a descriptive error (e.g.,
"Rate limited").Example Usage
Reference Implementation
Authors