WOT_SPEC.md raw

Web of Trust (WoT) Data Model Specification for Neo4j

This document describes the Web of Trust graph data model extensions for the ORLY Neo4j backend, based on the Brainstorm prototype.

Overview

The WoT data model extends the base Nostr relay functionality with trust metrics computation using graph algorithms (GrapeRank, Personalized PageRank) to enable:

Reference Implementation

Data Model Architecture

The WoT model adds specialized nodes and relationships to track social graph structure and compute trust metrics.

Node Labels

1. NostrUser

Represents a Nostr user (identified by pubkey) with computed trust metrics.

Properties:

Trust Metrics (Owner-Personalized):

Social Graph Counts:

Verified Counts (GrapeRank-weighted):

Input Scores (Sum of Influence):

NIP-56 Report Types:

For each report type (impersonator, spam, illegal, malware, nsfw, etc.), the following metrics are tracked:

Note: NIP-56 metrics may be better modeled as separate nodes to avoid property explosion.

Indexes:

2. SetOfNostrUserWotMetricsCards

Organizational node that groups all WoT metric cards for a single observee (user being scored). This design pattern keeps WoT metric cards partitioned from other NostrUser relationships.

Properties:

Purpose: Acts as an intermediary to minimize direct relationships on NostrUser nodes, which may have many other relationships in a full relay implementation.

Indexes:

3. NostrUserWotMetricsCard

Stores personalized trust metrics for a specific (observer, observee) pair. Each card corresponds to a NIP-85 Trusted Assertion (kind 30382) event.

Properties:

Trust Metrics (Observer-Personalized): All the same metrics as NostrUser node, but personalized to the observer:

Indexes:

4. Set (Deprecated)

Legacy node label that is redundant with SetOfNostrUserWotMetricsCards. Should be removed in new implementations.

Relationship Types

Tag-Based References (e and p tags)

The Neo4j backend uses a unified Tag-based model for e and p tags, enabling consistent tag querying while maintaining graph traversal capabilities.

E-tags (Event References):

(Event)-[:TAGGED_WITH]->(Tag {type: 'e', value: <event_id>})-[:REFERENCES]->(Event)

P-tags (Pubkey Mentions):

(Event)-[:TAGGED_WITH]->(Tag {type: 'p', value: <pubkey>})-[:REFERENCES]->(NostrUser)

This model provides:

Query Examples:

-- Find all events that reference a specific event
MATCH (e:Event)-[:TAGGED_WITH]->(t:Tag {type: 'e', value: $eventId})-[:REFERENCES]->(ref:Event)
RETURN e

-- Find all events that mention a specific pubkey
MATCH (e:Event)-[:TAGGED_WITH]->(t:Tag {type: 'p', value: $pubkey})-[:REFERENCES]->(u:NostrUser)
RETURN e

-- Count references to an event (thread replies)
MATCH (t:Tag {type: 'e', value: $eventId})<-[:TAGGED_WITH]-(e:Event)
RETURN count(e) AS replyCount

1. FOLLOWS

Represents a follow relationship between users (derived from kind 3 events).

Direction: (follower:NostrUser)-[:FOLLOWS]->(followed:NostrUser)

Properties: None (or optionally timestamp)

Source: Created/updated from kind 3 (contact list) events

2. MUTES

Represents a mute relationship between users (derived from kind 10000 events).

Direction: (muter:NostrUser)-[:MUTES]->(muted:NostrUser)

Properties: None (or optionally timestamp)

Source: Created/updated from kind 10000 (mute list) events

3. REPORTS

Represents a report filed against a user (derived from kind 1984 events).

Direction: (reporter:NostrUser)-[:REPORTS]->(reported:NostrUser)

Deduplication: Only one REPORTS relationship exists per (reporter, reported, report_type) combination. Multiple reports of the same type from the same user to the same target update the existing relationship with the most recent event's data. This prevents double-counting in GrapeRank calculations while maintaining audit trails via ProcessedSocialEvent nodes.

Properties:

Source: Created/updated from kind 1984 (reporting) events

4. WOTMETRICSCARDS

Links a NostrUser to their SetOfNostrUserWotMetricsCards organizational node.

Direction: (user:NostrUser)-[:WOT_METRICS_CARDS]->(set:SetOfNostrUserWotMetricsCards)

Properties: None

Cardinality: One-to-one (each NostrUser has at most one SetOfNostrUserWotMetricsCards)

5. SPECIFIC_INSTANCE

Links a SetOfNostrUserWotMetricsCards to individual NostrUserWotMetricsCard nodes for each observer.

Direction: (set:SetOfNostrUserWotMetricsCards)-[:SPECIFIC_INSTANCE]->(card:NostrUserWotMetricsCard)

Properties: None

Cardinality: One-to-many (one set has many cards, one per observer)

Note: May be renamed to WOT_METRICS_CARD for clarity.

Nostr Event Kinds

The WoT model processes the following Nostr event kinds:

KindNamePurposeGraph Action
0Profile MetadataUser profile informationUpdate NostrUser properties (npub, name, etc.)
3Contact ListFollow listCreate/update FOLLOWS relationships
1984ReportingReport users/contentCreate/update REPORTS relationships (deduplicated by report_type)
10000Mute ListMute listCreate/update MUTES relationships
30382Trusted Assertion (NIP-85)Published trust metricsCreate/update NostrUserWotMetricsCard nodes

Trust Metrics Computation

User Tracking Criteria

Trust metrics are computed for users who meet any of these criteria:

  1. Connected to the owner/observer by a finite number of FOLLOWS relationships (e.g., within N hops)
  2. Muted by a trusted user (user with sufficient influence)
  3. Reported by a trusted user

This typically results in ~300k tracked users out of millions in the network.

GrapeRank Algorithm

GrapeRank is a trust scoring algorithm that computes:

Note: Implementation details for GrapeRank are not included in the specification.

Personalized PageRank

Computes a personalized PageRank score for each user relative to an owner/observer, using the FOLLOWS graph as the link structure.

Note: Implementation details are not included in the specification.

Verified Counts

Users with influence above a configurable threshold are considered "verified" for counting purposes. This provides a quality-weighted count of followers/muters/reporters.

Input Scores

Alternative to verified counts: sum the influence scores of all followers/muters/reporters to get a weighted measure of social signals.

Deployment Modes

Lean Mode (Baseline)

Minimal WoT implementation suitable for resource-constrained deployments:

Hardware: Can run on smaller instances (e.g., 8 GB RAM, 2 vCPU)

Full Relay Mode (Extended)

Comprehensive implementation with additional features:

- IS_A_REACTION_TO (kind 7 reactions) - IS_A_RESPONSE_TO (kind 1 replies) - IS_A_REPOST_OF (kind 6, kind 16 reposts) - Tag-based references (see "Tag-Based References" section above): - Event-[:TAGGED_WITH]->Tag{type:'p'}-[:REFERENCES]->NostrUser (p-tag mentions) - Event-[:TAGGED_WITH]->Tag{type:'e'}-[:REFERENCES]->Event (e-tag references)

Hardware: Requires larger instances (e.g., 32 GB RAM, 8 vCPU, 100+ GB SSD)

Cypher Schema Definitions

-- NostrUser node constraint and indexes
CREATE CONSTRAINT nostrUser_pubkey IF NOT EXISTS
  FOR (n:NostrUser) REQUIRE n.pubkey IS UNIQUE;

CREATE INDEX nostrUser_hops IF NOT EXISTS
  FOR (n:NostrUser) ON (n.hops);

CREATE INDEX nostrUser_personalizedPageRank IF NOT EXISTS
  FOR (n:NostrUser) ON (n.personalizedPageRank);

CREATE INDEX nostrUser_influence IF NOT EXISTS
  FOR (n:NostrUser) ON (n.influence);

CREATE INDEX nostrUser_verifiedFollowerCount IF NOT EXISTS
  FOR (n:NostrUser) ON (n.verifiedFollowerCount);

CREATE INDEX nostrUser_verifiedMuterCount IF NOT EXISTS
  FOR (n:NostrUser) ON (n.verifiedMuterCount);

CREATE INDEX nostrUser_verifiedReporterCount IF NOT EXISTS
  FOR (n:NostrUser) ON (n.verifiedReporterCount);

CREATE INDEX nostrUser_followerInput IF NOT EXISTS
  FOR (n:NostrUser) ON (n.followerInput);

-- SetOfNostrUserWotMetricsCards constraint
CREATE CONSTRAINT SetOfNostrUserWotMetricsCards_observee_pubkey IF NOT EXISTS
  FOR (n:SetOfNostrUserWotMetricsCards) REQUIRE n.observee_pubkey IS UNIQUE;

-- NostrUserWotMetricsCard constraints and indexes
CREATE CONSTRAINT nostrUserWotMetricsCard_unique_combination_1 IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) REQUIRE (n.customer_id, n.observee_pubkey) IS UNIQUE;

CREATE CONSTRAINT nostrUserWotMetricsCard_unique_combination_2 IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) REQUIRE (n.observer_pubkey, n.observee_pubkey) IS UNIQUE;

CREATE INDEX nostrUserWotMetricsCard_customer_id IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.customer_id);

CREATE INDEX nostrUserWotMetricsCard_observer_pubkey IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.observer_pubkey);

CREATE INDEX nostrUserWotMetricsCard_observee_pubkey IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.observee_pubkey);

CREATE INDEX nostrUserWotMetricsCard_hops IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.hops);

CREATE INDEX nostrUserWotMetricsCard_personalizedPageRank IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.personalizedPageRank);

CREATE INDEX nostrUserWotMetricsCard_influence IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.influence);

CREATE INDEX nostrUserWotMetricsCard_verifiedFollowerCount IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.verifiedFollowerCount);

CREATE INDEX nostrUserWotMetricsCard_verifiedMuterCount IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.verifiedMuterCount);

CREATE INDEX nostrUserWotMetricsCard_verifiedReporterCount IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.verifiedReporterCount);

CREATE INDEX nostrUserWotMetricsCard_followerInput IF NOT EXISTS
  FOR (n:NostrUserWotMetricsCard) ON (n.followerInput);

Example Queries

Find users followed by owner within N hops

MATCH path = (owner:NostrUser {pubkey: $ownerPubkey})-[:FOLLOWS*1..3]->(user:NostrUser)
WHERE user.hops <= 3
RETURN user.pubkey, user.hops, user.influence
ORDER BY user.influence DESC
LIMIT 100

Get trust metrics for a specific observer-observee pair

MATCH (card:NostrUserWotMetricsCard {
  observer_pubkey: $observerPubkey,
  observee_pubkey: $observeePubkey
})
RETURN card.hops, card.influence, card.personalizedPageRank

Find highly trusted users (high influence, many verified followers)

MATCH (user:NostrUser)
WHERE user.influence > $threshold
  AND user.verifiedFollowerCount > $minFollowers
RETURN user.pubkey, user.influence, user.verifiedFollowerCount
ORDER BY user.influence DESC
LIMIT 50

Find reported users with high reporter influence

MATCH (reporter:NostrUser)-[r:REPORTS]->(reported:NostrUser)
WHERE reporter.influence > $threshold
RETURN reported.pubkey,
       r.reportType,
       COUNT(reporter) AS reportCount,
       SUM(reporter.influence) AS totalInfluence
ORDER BY totalInfluence DESC

Integration with ORLY Relay

Configuration

# Enable Neo4j backend
export ORLY_DB_TYPE=neo4j
export ORLY_NEO4J_URI=bolt://localhost:7687
export ORLY_NEO4J_USER=neo4j
export ORLY_NEO4J_PASSWORD=password

# Enable WoT processing
export ORLY_WOT_ENABLED=true
export ORLY_WOT_OWNER_PUBKEY=<hex-pubkey>
export ORLY_WOT_INFLUENCE_THRESHOLD=0.5
export ORLY_WOT_MAX_HOPS=3

# Enable multi-tenant support
export ORLY_WOT_MULTI_TENANT=true

Event Processing Flow

  1. Kind 0 (Profile): Update NostrUser node properties
  2. Kind 3 (Follows): Parse p-tags, create/update FOLLOWS relationships
  3. Kind 1984 (Reports): Parse p-tags and report type, create REPORTS relationships
  4. Kind 10000 (Mutes): Parse p-tags, create/update MUTES relationships
  5. Background Job: Periodically run GrapeRank and PageRank algorithms
  6. Kind 30382 (Trusted Assertion): Update NostrUserWotMetricsCard nodes

Query Filtering

Extend REQ filters with WoT parameters:

{
  "kinds": [1],
  "wot": {
    "max_hops": 2,
    "min_influence": 0.5,
    "observer": "<pubkey>"
  }
}

Performance Considerations

Future Enhancements

References