- Change processReport() to use MERGE instead of CREATE for REPORTS relationships, deduplicating by (reporter, reported, report_type) - Add ON CREATE/ON MATCH clauses to preserve newest event data while preventing duplicate relationships - Add getExistingReportEvent() helper to check for existing reports - Add markReportEventSuperseded() to track superseded events - Add v4 migration migrateDeduplicateReports() to clean up existing duplicate REPORTS relationships in databases - Add comprehensive tests: TestReportDeduplication with subtests for deduplication, different types, and superseded event tracking - Update WOT_SPEC.md with REPORTS deduplication behavior and correct property names (report_type, created_at, created_by_event) - Bump version to v0.36.1 Fixes: https://git.nostrdev.com/mleku/next.orly.dev/issues/16 Files modified: - pkg/neo4j/social-event-processor.go: MERGE-based deduplication - pkg/neo4j/migrations.go: v4 migration for duplicate cleanup - pkg/neo4j/social-event-processor_test.go: Deduplication tests - pkg/neo4j/WOT_SPEC.md: Updated REPORTS documentation - pkg/version/version: Bump to v0.36.1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
18 KiB
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:
- Social graph-based filtering: Filter events based on web of trust relationships
- Personalized trust scores: Compute trust metrics personalized to each user/customer
- Multi-tenant support: Track separate trust metrics for multiple customers/observers
- Spam and moderation: Use social graph signals (follows, mutes, reports) for content filtering
Reference Implementation
- Live instance: https://straycat.brainstorm.social (32 GB RAM, 8 vCPU, 100 GB SSD)
- Repository: https://github.com/Pretty-Good-Freedom-Tech/brainstorm
- Neo4j browser: http://straycat.brainstorm.social:7474/browser/
- Relay: https://straycat.brainstorm.social/relay
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:
pubkey(string, unique) - Hex-encoded public keynpub(string) - Bech32-encoded npub
Trust Metrics (Owner-Personalized):
hops(integer) - Distance from owner node via FOLLOWS relationshipspersonalizedPageRank(float) - PageRank score personalized to ownerinfluence(float) - GrapeRank influence scoreaverage(float) - GrapeRank average scoreinput(float) - GrapeRank input scoreconfidence(float) - GrapeRank confidence score
Social Graph Counts:
followingCount(integer) - Total number of users this user followsfollowedByCount(integer) - Total number of followersmutingCount(integer) - Total number of users this user mutesmutedByCount(integer) - Total number of users who mute this userreportingCount(integer) - Total number of reports filed by this userreportedByCount(integer) - Total number of reports filed against this user
Verified Counts (GrapeRank-weighted):
verifiedFollowerCount(integer) - Count of followers with influence above thresholdverifiedMuterCount(integer) - Count of muters with influence above thresholdverifiedReporterCount(integer) - Count of reporters with influence above threshold
Input Scores (Sum of Influence):
followerInput(float) - Sum of influence scores of all followersmuterInput(float) - Sum of influence scores of all mutersreporterInput(float) - Sum of influence scores of all reporters
NIP-56 Report Types:
For each report type (impersonator, spam, illegal, malware, nsfw, etc.), the following metrics are tracked:
{reportType}Count(integer) - Total count of this report type{reportType}VerifiedCount(integer) - Count from verified reporters{reportType}Input(float) - Sum of influence scores of reporters
Note: NIP-56 metrics may be better modeled as separate nodes to avoid property explosion.
Indexes:
- Unique constraint on
pubkey - Index on
hops - Index on
personalizedPageRank - Index on
influence - Index on
verifiedFollowerCount - Index on
verifiedMuterCount - Index on
verifiedReporterCount - Index on
followerInput
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:
observee_pubkey(string, unique) - Pubkey of the user being scored
Purpose: Acts as an intermediary to minimize direct relationships on NostrUser nodes, which may have many other relationships in a full relay implementation.
Indexes:
- Unique constraint on
observee_pubkey
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:
customer_id(string) - Identifier for the customer/service instanceobserver_pubkey(string) - Pubkey of the observer (the customer)observee_pubkey(string) - Pubkey of the user being scored
Trust Metrics (Observer-Personalized): All the same metrics as NostrUser node, but personalized to the observer:
hops,personalizedPageRankinfluence,average,input,confidenceverifiedFollowerCount,verifiedMuterCount,verifiedReporterCountfollowerInput,muterInput,reporterInput
Indexes:
- Unique constraint on
(customer_id, observee_pubkey) - Unique constraint on
(observer_pubkey, observee_pubkey) - Index on
customer_id - Index on
observer_pubkey - Index on
observee_pubkey - Index on
hops - Index on
personalizedPageRank - Index on
influence - Index on
verifiedFollowerCount - Index on
verifiedMuterCount - Index on
verifiedReporterCount - Index on
followerInput
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:
- Unified tag querying via
#eand#pfilters (same as other tags) - Graph traversal from events to referenced events/users
- Consistent indexing through existing Tag node indexes
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:
report_type(string) - NIP-56 report type (impersonation, spam, illegal, malware, nsfw, etc.)created_at(integer) - Timestamp of the most recent report eventcreated_by_event(string) - Event ID of the most recent reportrelay_received_at(integer) - When the relay first received any report of this type
Source: Created/updated from kind 1984 (reporting) events
4. WOT_METRICS_CARDS
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:
| Kind | Name | Purpose | Graph Action |
|---|---|---|---|
| 0 | Profile Metadata | User profile information | Update NostrUser properties (npub, name, etc.) |
| 3 | Contact List | Follow list | Create/update FOLLOWS relationships |
| 1984 | Reporting | Report users/content | Create/update REPORTS relationships (deduplicated by report_type) |
| 10000 | Mute List | Mute list | Create/update MUTES relationships |
| 30382 | Trusted Assertion (NIP-85) | Published trust metrics | Create/update NostrUserWotMetricsCard nodes |
Trust Metrics Computation
User Tracking Criteria
Trust metrics are computed for users who meet any of these criteria:
- Connected to the owner/observer by a finite number of FOLLOWS relationships (e.g., within N hops)
- Muted by a trusted user (user with sufficient influence)
- 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:
- Influence: Primary trust score based on social graph structure
- Average: Average trust received from neighbors
- Input: Total trust input from all connections
- Confidence: Confidence level in the score
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:
- NostrUser, NostrUserWotMetricsCard, SetOfNostrUserWotMetricsCards nodes
- FOLLOWS, MUTES, REPORTS, WOT_METRICS_CARDS, SPECIFIC_INSTANCE relationships
- Process kinds: 0, 3, 1984, 10000
- Compute baseline trust metrics
Hardware: Can run on smaller instances (e.g., 8 GB RAM, 2 vCPU)
Full Relay Mode (Extended)
Comprehensive implementation with additional features:
- All lean mode features
- NostrEvent nodes with full event storage
- Additional relationships:
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)
- NostrRelay, CashuMint nodes for ecosystem mapping
- Enhanced GrapeRank incorporating zaps, replies, reactions
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
- Kind 0 (Profile): Update NostrUser node properties
- Kind 3 (Follows): Parse p-tags, create/update FOLLOWS relationships
- Kind 1984 (Reports): Parse p-tags and report type, create REPORTS relationships
- Kind 10000 (Mutes): Parse p-tags, create/update MUTES relationships
- Background Job: Periodically run GrapeRank and PageRank algorithms
- 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
- Index Strategy: Heavy indexing on trust metric fields for fast filtering
- Batch Updates: Process social graph events in batches to minimize graph writes
- Cached Metrics: Store computed trust metrics as node properties (denormalized)
- Incremental Computation: Update metrics incrementally when graph changes
- Query Optimization: Use Cypher query plans (EXPLAIN/PROFILE) to optimize complex traversals
Future Enhancements
- NIP-56 report type nodes (separate from NostrUser properties)
- Full relay mode with NostrEvent nodes
- Zap-weighted trust metrics
- Reply/reaction-weighted trust metrics
- Distributed trust computation across multiple relay instances
- Real-time trust metric updates (streaming)
References
- NIP-56 (Reporting): https://github.com/nostr-protocol/nips/blob/master/56.md
- NIP-85 (Trusted Assertions): https://nostrhub.io/naddr1qvzqqqrcvypzq3svyhng9ld8sv44950j957j9vchdktj7cxumsep9mvvjthc2pjuqyt8wumn8ghj7un9d3shjtnswf5k6ctv9ehx2aqqzf68yatnw3jkgttpwdek2un5d9hkuuctys9zn
- Brainstorm Prototype: https://github.com/Pretty-Good-Freedom-Tech/brainstorm
- NIP-56 Metrics Dashboard: https://straycat.brainstorm.social/nip56.html