Web of Trust (WOT)
@nostr-dev-kit/wot
provides Web of Trust utilities for filtering and ranking content based on your social graph.
Installation
npm install @nostr-dev-kit/wot
Quick Start
import NDK from "@nostr-dev-kit/ndk";
import { NDKWoT, filterByWoT, rankByWoT } from "@nostr-dev-kit/wot";
const ndk = new NDK();
await ndk.connect();
// Build WOT graph from your perspective
const wot = new NDKWoT(ndk, myPubkey);
await wot.load({
depth: 2, // 2 hops out from you
maxFollows: 1000, // limit follows per user
timeout: 30000 // 30s timeout
});
console.log(`WOT has ${wot.size} users`);
Filtering Events
The key insight of WOT is to fetch broadly, filter locally. This lets you adjust WOT strictness without re-fetching from relays.
// Fetch events broadly
const events = await ndk.fetchEvents({
kinds: [1],
limit: 500
});
// Filter by WOT
const filtered = filterByWoT(wot, events, {
maxDepth: 2, // only users within 2 hops
minScore: 0.5, // minimum WOT score (0-1)
includeUnknown: false // exclude users outside WOT
});
// Easily adjust view without re-fetching
const broader = filterByWoT(wot, events, { maxDepth: 3 });
const strictest = filterByWoT(wot, events, { maxDepth: 1 });
WOT as Automatic Mute Filter
You can integrate WOT with NDK's muteFilter
to automatically filter out users outside your web of trust at the NDK level. This means all subscriptions and event fetches will automatically exclude non-WOT users.
import NDK, { NDKEvent } from "@nostr-dev-kit/ndk";
import { NDKWoT } from "@nostr-dev-kit/wot";
const ndk = new NDK();
await ndk.connect();
// Build WOT graph
const wot = new NDKWoT(ndk, myPubkey);
await wot.load({ depth: 2 });
// Set WOT-based mute filter
ndk.muteFilter = (event: NDKEvent) => {
// Check manual mutes first
if (ndk.mutedIds.has(event.pubkey)) return true;
if (event.id && ndk.mutedIds.has(event.id)) return true;
// Auto-mute users outside WOT
if (!wot.includes(event.pubkey, { maxDepth: 2 })) {
return true; // Mute this event
}
return false; // Don't mute
};
// Now all subscriptions automatically filter by WOT
const sub = ndk.subscribe({ kinds: [1], limit: 100 });
sub.on('event', (event) => {
// Only events from WOT users will appear here
console.log(event.content);
});
Hybrid Approach: WOT + Manual Mutes + Keywords
Combine WOT with manual mutes and content filtering:
ndk.muteFilter = (event: NDKEvent) => {
// 1. Manual mutes (highest priority)
if (ndk.mutedIds.has(event.pubkey)) return true;
if (event.id && ndk.mutedIds.has(event.id)) return true;
// 2. WOT check (only for unknown users)
if (!wot.includes(event.pubkey, { maxDepth: 2 })) {
return true; // Auto-mute non-WOT users
}
// 3. Content filtering (even for WOT users)
const blockedWords = ['spam', 'scam'];
if (blockedWords.some(word => event.content.toLowerCase().includes(word))) {
return true;
}
return false;
};
Adjustable WOT Strictness
Allow users to adjust WOT strictness dynamically:
let wotDepth = 2; // Default: 2 hops
ndk.muteFilter = (event: NDKEvent) => {
if (ndk.mutedIds.has(event.pubkey)) return true;
// Use current depth setting
return !wot.includes(event.pubkey, { maxDepth: wotDepth });
};
// User can adjust strictness
function setWoTStrictness(depth: number) {
wotDepth = depth;
// Trigger UI refresh to re-apply filter
}
// Slider: 1 (strictest) -> 3 (most permissive)
setWoTStrictness(1); // Only direct follows
setWoTStrictness(2); // Friends of friends
setWoTStrictness(3); // 3 hops out
WOT-Based Score Thresholds
Filter by WOT score instead of depth:
const MIN_WOT_SCORE = 0.5; // 0-1 scale
ndk.muteFilter = (event: NDKEvent) => {
if (ndk.mutedIds.has(event.pubkey)) return true;
// Mute if score too low
const score = wot.getScore(event.pubkey);
if (score < MIN_WOT_SCORE) {
return true;
}
return false;
};
Ranking Events
Sort events by WOT proximity:
// Rank by distance (closer = higher)
const ranked = rankByWoT(wot, events, {
algorithm: "distance", // "distance", "score", or "followers"
unknownsLast: true // put unknown users at the end
});
// Custom ranking
const custom = rankByWoT(wot, events, {
comparator: (a, b) => {
const aScore = wot.getScore(a.pubkey);
const bScore = wot.getScore(b.pubkey);
return bScore - aScore;
}
});
WOT Queries
// Check if user is in WOT
if (wot.includes(pubkey, { maxDepth: 2 })) {
console.log("User is in WOT");
}
// Get WOT score (0-1, higher = closer)
const score = wot.getScore(pubkey);
// Get distance (hops from root)
const distance = wot.getDistance(pubkey); // returns number or null
// Get scores for multiple users
const scores = wot.getScores([pubkey1, pubkey2, pubkey3]);
// Get all pubkeys in WOT
const allUsers = wot.getAllPubkeys({ maxDepth: 2 });
Ranking Algorithms
Distance
Ranks by graph distance from root user. Closer users rank higher.
rankByWoT(wot, events, { algorithm: "distance" });
Score
Ranks by WOT score (inverse of depth: 1 / (depth + 1)
). Higher scores rank higher.
rankByWoT(wot, events, { algorithm: "score" });
Followers
Ranks by number of WOT users following them. More popular within your WOT ranks higher.
rankByWoT(wot, events, { algorithm: "followers" });
Integration with Reactive Frameworks
Use with svelte
or ndk-hooks
for reactive WOT views:
Svelte 5 (coming soon)
import { wotView } from '@nostr-dev-kit/svelte';
const view = wotView(wot, events, {
maxDepth: $state(2),
showUnknowns: $state(false)
});
// Reactive
$effect(() => {
console.log(view.filtered);
});
React (coming soon)
import { useWoTView } from '@nostr-dev-kit/react';
function Feed() {
const [depth, setDepth] = useState(2);
const { filtered, ranked } = useWoTView(wot, events, {
maxDepth: depth
});
return <EventList events={filtered} />;
}
API Reference
NDKWoT
constructor(ndk: NDK, rootPubkey: string)
- Create WOT instanceload(options: WoTBuildOptions): Promise<void>
- Build the graphgetScore(pubkey: string): number
- Get WOT score (0-1)getDistance(pubkey: string): number | null
- Get hops from rootincludes(pubkey: string, options?): boolean
- Check if in WOTgetAllPubkeys(options?): string[]
- Get all pubkeys in WOTgetScores(pubkeys: string[]): Map<string, number>
- Batch scoresgetNode(pubkey: string): WoTNode | null
- Get WOT node detailssize: number
- Total nodes in graphisLoaded(): boolean
- Check if graph is loaded
Filter Functions
filterByWoT
filterByWoT(
wot: NDKWoT,
events: NDKEvent[],
options: WoTFilterOptions
): NDKEvent[]
rankByWoT
rankByWoT(
wot: NDKWoT,
events: NDKEvent[],
options: WoTRankOptions
): NDKEvent[]
createWoTComparator
createWoTComparator(
wot: NDKWoT,
options: WoTRankOptions
): (a: NDKEvent, b: NDKEvent) => number
Types
WoTBuildOptions
interface WoTBuildOptions {
depth: number; // Max hops to traverse
maxFollows?: number; // Max follows per user
timeout?: number; // Timeout in ms
}
WoTFilterOptions
interface WoTFilterOptions {
maxDepth?: number; // Max depth to include
minScore?: number; // Min WOT score (0-1)
includeUnknown?: boolean; // Include non-WOT users
}
WoTRankOptions
interface WoTRankOptions {
algorithm?: "distance" | "score" | "followers";
unknownsLast?: boolean;
comparator?: (a: NDKEvent, b: NDKEvent) => number;
}
WoTNode
interface WoTNode {
pubkey: string;
depth: number; // Hops from root
followedBy: Set<string>; // Who follows this user
}