Files
next.orly.dev/docs/FIND_RATE_LIMITING_MECHANISMS.md

27 KiB

FIND Rate Limiting Mechanisms (Non-Monetary, Non-PoW)

Overview

This document explores mechanisms to rate limit name registrations in the FIND protocol without requiring:

  • Security deposits or payments
  • Monetary mechanisms (Lightning, ecash, etc.)
  • Proof of work (computational puzzles)

The goal is to prevent spam and name squatting while maintaining decentralization and accessibility.


1. Time-Based Mechanisms

1.1 Proposal-to-Ratification Delay

Concept: Mandatory waiting period between submitting a registration proposal and consensus ratification.

Implementation:

type ProposalDelay struct {
    MinDelay    time.Duration // e.g., 1 hour
    MaxDelay    time.Duration // e.g., 24 hours
    GracePeriod time.Duration // Random jitter to prevent timing attacks
}

func (r *RegistryService) validateProposalTiming(proposal *Proposal) error {
    elapsed := time.Since(proposal.CreatedAt)
    minRequired := r.config.ProposalDelay.MinDelay

    if elapsed < minRequired {
        return fmt.Errorf("proposal must age %v before ratification (current: %v)",
            minRequired, elapsed)
    }

    return nil
}

Advantages:

  • Simple to implement
  • Gives community time to review and object
  • Prevents rapid-fire squatting
  • Allows for manual intervention in disputes

Disadvantages:

  • Poor UX (users wait hours/days)
  • Doesn't prevent determined attackers with patience
  • Vulnerable to timing attacks (frontrunning)

Variations:

  • Progressive Delays: First name = 1 hour, second = 6 hours, third = 24 hours, etc.
  • Random Delays: Each proposal gets random delay within range to prevent prediction
  • Peak-Time Penalties: Longer delays during high registration volume

1.2 Per-Account Cooldown Periods

Concept: Limit how frequently a single npub can register names.

Implementation:

type RateLimiter struct {
    registrations map[string][]time.Time // npub -> registration timestamps
    cooldown      time.Duration          // e.g., 7 days
    maxPerPeriod  int                    // e.g., 3 names per week
}

func (r *RateLimiter) canRegister(npub string, now time.Time) (bool, time.Duration) {
    timestamps := r.registrations[npub]

    // Remove expired timestamps
    cutoff := now.Add(-r.cooldown)
    active := filterAfter(timestamps, cutoff)

    if len(active) >= r.maxPerPeriod {
        oldestExpiry := active[0].Add(r.cooldown)
        waitTime := oldestExpiry.Sub(now)
        return false, waitTime
    }

    return true, 0
}

Advantages:

  • Directly limits per-user registration rate
  • Configurable (relays can set own limits)
  • Persistent across sessions

Disadvantages:

  • Easy to bypass with multiple npubs
  • Requires state tracking across registry services
  • May be too restrictive for legitimate bulk registrations

Variations:

  • Sliding Window: Count registrations in last N days
  • Token Bucket: Allow bursts but enforce long-term average
  • Decay Model: Cooldown decreases over time (1 day → 6 hours → 1 hour)

1.3 Account Age Requirements

Concept: Npubs must be a certain age before they can register names.

Implementation:

func (r *RegistryService) validateAccountAge(npub string, minAge time.Duration) error {
    // Query oldest event from this npub across known relays
    oldestEvent, err := r.getOldestEventByAuthor(npub)
    if err != nil {
        return fmt.Errorf("cannot determine account age: %w", err)
    }

    accountAge := time.Since(oldestEvent.CreatedAt)
    if accountAge < minAge {
        return fmt.Errorf("account must be %v old (current: %v)", minAge, accountAge)
    }

    return nil
}

Advantages:

  • Prevents throwaway account spam
  • Encourages long-term participation
  • No ongoing cost to users

Disadvantages:

  • Barrier for new users
  • Can be gamed with pre-aged accounts
  • Requires historical event data

Variations:

  • Tiered Ages: Basic names require 30 days, premium require 90 days
  • Activity Threshold: Not just age, but "active" age (X events published)

2. Web of Trust (WoT) Mechanisms

2.1 Follow Count Requirements

Concept: Require minimum follow count from trusted accounts to register names.

Implementation:

type WoTValidator struct {
    minFollowers    int      // e.g., 5 followers
    trustedAccounts []string // Bootstrap trusted npubs
}

func (v *WoTValidator) validateFollowCount(npub string) error {
    // Query kind 3 events that include this npub in follow list
    followers, err := v.queryFollowers(npub)
    if err != nil {
        return err
    }

    // Count only followers who are themselves trusted
    trustedFollowers := 0
    for _, follower := range followers {
        if v.isTrusted(follower) {
            trustedFollowers++
        }
    }

    if trustedFollowers < v.minFollowers {
        return fmt.Errorf("need %d trusted followers, have %d",
            v.minFollowers, trustedFollowers)
    }

    return nil
}

Advantages:

  • Leverages existing Nostr social graph
  • Self-regulating (community decides who's trusted)
  • Sybil-resistant if trust graph is diverse

Disadvantages:

  • Chicken-and-egg for new users
  • Can create gatekeeping
  • Vulnerable to follow-for-follow schemes

Variations:

  • Weighted Followers: High-reputation followers count more
  • Mutual Follows: Require bidirectional relationships
  • Follow Depth: Count 2-hop or 3-hop follows

2.2 Endorsement/Vouching System

Concept: Existing name holders can vouch for new registrants.

Implementation:

// Kind 30110: Name Registration Endorsement
type Endorsement struct {
    Voucher   string // npub of existing name holder
    Vouchee   string // npub seeking registration
    NamesSeen int    // How many names voucher has endorsed (spam detection)
}

func (r *RegistryService) validateEndorsements(proposal *Proposal) error {
    // Query endorsements for this npub
    endorsements, err := r.queryEndorsements(proposal.Author)
    if err != nil {
        return err
    }

    // Require at least 2 endorsements from different name holders
    uniqueVouchers := make(map[string]bool)
    for _, e := range endorsements {
        // Check voucher holds a name
        if r.holdsActiveName(e.Voucher) {
            uniqueVouchers[e.Voucher] = true
        }
    }

    if len(uniqueVouchers) < 2 {
        return fmt.Errorf("need 2 endorsements from name holders, have %d",
            len(uniqueVouchers))
    }

    return nil
}

Advantages:

  • Creates social accountability
  • Name holders have "skin in the game"
  • Can revoke endorsements if abused

Disadvantages:

  • Requires active participation from name holders
  • Can create favoritism/cliques
  • Vouchers may sell endorsements

Variations:

  • Limited Vouches: Each name holder can vouch for max N users per period
  • Reputation Cost: Vouching for spammer reduces voucher's reputation
  • Delegation Chains: Vouched users can vouch others (with decay)

2.3 Activity History Requirements

Concept: Require meaningful Nostr activity before allowing registration.

Implementation:

type ActivityRequirements struct {
    MinEvents      int           // e.g., 50 events
    MinTimespan    time.Duration // e.g., 30 days
    RequiredKinds  []int         // Must have posted notes, not just kind 0
    MinUniqueRelays int          // Must use multiple relays
}

func (r *RegistryService) validateActivity(npub string, reqs ActivityRequirements) error {
    events, err := r.queryUserEvents(npub)
    if err != nil {
        return err
    }

    // Check event count
    if len(events) < reqs.MinEvents {
        return fmt.Errorf("need %d events, have %d", reqs.MinEvents, len(events))
    }

    // Check timespan
    oldest := events[0].CreatedAt
    newest := events[len(events)-1].CreatedAt
    timespan := newest.Sub(oldest)
    if timespan < reqs.MinTimespan {
        return fmt.Errorf("activity must span %v, current span: %v",
            reqs.MinTimespan, timespan)
    }

    // Check event diversity
    kinds := make(map[int]bool)
    for _, e := range events {
        kinds[e.Kind] = true
    }

    hasRequiredKinds := true
    for _, kind := range reqs.RequiredKinds {
        if !kinds[kind] {
            hasRequiredKinds = false
            break
        }
    }

    if !hasRequiredKinds {
        return fmt.Errorf("missing required event kinds")
    }

    return nil
}

Advantages:

  • Rewards active community members
  • Hard to fake authentic activity
  • Aligns with Nostr values (participation)

Disadvantages:

  • High barrier for new users
  • Can be gamed with bot activity
  • Definition of "meaningful" is subjective

Variations:

  • Engagement Metrics: Require replies, reactions, zaps received
  • Content Quality: Use NIP-32 labels to filter quality content
  • Relay Diversity: Must have published to N different relays

3. Multi-Phase Verification

3.1 Two-Phase Commit with Challenge

Concept: Proposal → Challenge → Response → Ratification

Implementation:

// Phase 1: Submit proposal (kind 30100)
type RegistrationProposal struct {
    Name   string
    Action string // "register"
}

// Phase 2: Registry issues challenge (kind 20110)
type RegistrationChallenge struct {
    ProposalID string
    Challenge  string // Random challenge string
    IssuedAt   time.Time
    ExpiresAt  time.Time
}

// Phase 3: User responds (kind 20111)
type ChallengeResponse struct {
    ChallengeID string
    Response    string // Signed challenge
    ProposalID  string
}

func (r *RegistryService) processProposal(proposal *Proposal) {
    // Generate random challenge
    challenge := generateRandomChallenge()

    // Publish challenge event
    challengeEvent := &ChallengeEvent{
        ProposalID: proposal.ID,
        Challenge:  challenge,
        ExpiresAt:  time.Now().Add(5 * time.Minute),
    }
    r.publishChallenge(challengeEvent)

    // Wait for response
    // If valid response received within window, proceed with attestation
}

Advantages:

  • Proves user is actively monitoring
  • Prevents pre-signed bulk registrations
  • Adds friction without monetary cost

Disadvantages:

  • Requires active participation (can't be automated)
  • Poor UX (multiple steps)
  • Vulnerable to automated response systems

Variations:

  • Time-Delayed Challenge: Challenge issued X hours after proposal
  • Multi-Registry Challenges: Must respond to challenges from multiple services
  • Progressive Challenges: Later names require harder challenges

3.2 Multi-Signature Requirements

Concept: Require signatures from multiple devices/keys to prove human operator.

Implementation:

type MultiSigProposal struct {
    Name           string
    PrimaryKey     string   // Main npub
    SecondaryKeys  []string // Additional npubs that must co-sign
    Signatures     []Signature
}

func (r *RegistryService) validateMultiSig(proposal *MultiSigProposal) error {
    // Require at least 2 signatures from different keys
    if len(proposal.Signatures) < 2 {
        return fmt.Errorf("need at least 2 signatures")
    }

    // Verify each signature
    for _, sig := range proposal.Signatures {
        if !verifySignature(proposal.Name, sig) {
            return fmt.Errorf("invalid signature from %s", sig.Pubkey)
        }
    }

    // Ensure signatures are from different keys
    uniqueKeys := make(map[string]bool)
    for _, sig := range proposal.Signatures {
        uniqueKeys[sig.Pubkey] = true
    }

    if len(uniqueKeys) < 2 {
        return fmt.Errorf("signatures must be from distinct keys")
    }

    return nil
}

Advantages:

  • Harder to automate at scale
  • Proves access to multiple devices
  • No external dependencies

Disadvantages:

  • Complex UX (managing multiple keys)
  • Still bypassable with multiple hardware keys
  • May lose access if secondary key lost

4. Lottery and Randomization

4.1 Random Selection Among Competing Proposals

Concept: When multiple proposals for same name arrive, randomly select winner.

Implementation:

func (r *RegistryService) selectWinner(proposals []*Proposal) *Proposal {
    if len(proposals) == 1 {
        return proposals[0]
    }

    // Use deterministic randomness based on block hash or similar
    seed := r.getConsensusSeed() // From latest Bitcoin block hash, etc.

    // Create weighted lottery based on account age, reputation, etc.
    weights := make([]int, len(proposals))
    for i, p := range proposals {
        weights[i] = r.calculateWeight(p.Author)
    }

    // Select winner
    rng := rand.New(rand.NewSource(seed))
    winner := weightedRandomSelect(proposals, weights, rng)

    return winner
}

func (r *RegistryService) calculateWeight(npub string) int {
    // Base weight: 1
    weight := 1

    // +1 for each month of account age (max 12)
    accountAge := r.getAccountAge(npub)
    weight += min(int(accountAge.Hours()/730), 12)

    // +1 for each 100 events (max 10)
    eventCount := r.getEventCount(npub)
    weight += min(eventCount/100, 10)

    // +1 for each trusted follower (max 20)
    followerCount := r.getTrustedFollowerCount(npub)
    weight += min(followerCount, 20)

    return weight
}

Advantages:

  • Fair chance for all participants
  • Can weight by reputation without hard gatekeeping
  • Discourages squatting (no guarantee of winning)

Disadvantages:

  • Winners may feel arbitrary
  • Still requires sybil resistance (or attackers spam proposals)
  • Requires consensus on randomness source

Variations:

  • Time-Weighted Lottery: Earlier proposals have slightly higher odds
  • Reputation-Only Lottery: Only weight by WoT score
  • Periodic Lotteries: Batch proposals weekly, run lottery for all conflicts

4.2 Queue System with Priority Ranking

Concept: Proposals enter queue, priority determined by non-transferable metrics.

Implementation:

type ProposalQueue struct {
    proposals []*ScoredProposal
}

type ScoredProposal struct {
    Proposal *Proposal
    Score    int
}

func (r *RegistryService) scoreProposal(p *Proposal) int {
    score := 0

    // Account age contribution (0-30 points)
    accountAge := r.getAccountAge(p.Author)
    score += min(int(accountAge.Hours()/24), 30) // 1 point per day, max 30

    // Event count contribution (0-20 points)
    eventCount := r.getEventCount(p.Author)
    score += min(eventCount/10, 20) // 1 point per 10 events, max 20

    // WoT contribution (0-30 points)
    wotScore := r.getWoTScore(p.Author)
    score += min(wotScore, 30)

    // Endorsements (0-20 points)
    endorsements := r.getEndorsementCount(p.Author)
    score += min(endorsements*5, 20) // 5 points per endorsement, max 20

    return score
}

func (q *ProposalQueue) process() *Proposal {
    if len(q.proposals) == 0 {
        return nil
    }

    // Sort by score (descending)
    sort.Slice(q.proposals, func(i, j int) bool {
        return q.proposals[i].Score > q.proposals[j].Score
    })

    // Process highest score
    winner := q.proposals[0]
    q.proposals = q.proposals[1:]

    return winner.Proposal
}

Advantages:

  • Transparent, merit-based selection
  • Rewards long-term participation
  • Predictable for users (can see their score)

Disadvantages:

  • Complex scoring function
  • May favor old accounts over new legitimate users
  • Gaming possible if score calculation public

5. Behavioral Analysis

5.1 Pattern Detection

Concept: Detect and flag suspicious registration patterns.

Implementation:

type BehaviorAnalyzer struct {
    recentProposals map[string][]*Proposal // IP/relay -> proposals
    suspiciousScore map[string]int         // npub -> suspicion score
}

func (b *BehaviorAnalyzer) analyzeProposal(p *Proposal) (suspicious bool, reason string) {
    score := 0

    // Check registration frequency
    if b.recentProposalCount(p.Author, 1*time.Hour) > 5 {
        score += 20
    }

    // Check name similarity (registering foo1, foo2, foo3, ...)
    if b.hasSequentialNames(p.Author) {
        score += 30
    }

    // Check relay diversity (all from same relay = suspicious)
    if b.relayDiversity(p.Author) < 2 {
        score += 15
    }

    // Check timestamp patterns (all proposals at exact intervals)
    if b.hasRegularIntervals(p.Author) {
        score += 25
    }

    // Check for dictionary attack patterns
    if b.isDictionaryAttack(p.Author) {
        score += 40
    }

    if score > 50 {
        return true, b.generateReason(score)
    }

    return false, ""
}

Advantages:

  • Catches automated attacks
  • No burden on legitimate users
  • Adaptive (can tune detection rules)

Disadvantages:

  • False positives possible
  • Requires heuristic development
  • Attackers can adapt

Variations:

  • Machine Learning: Train model on spam vs. legitimate patterns
  • Collaborative Filtering: Share suspicious patterns across registry services
  • Progressive Restrictions: Suspicious users face longer delays

5.2 Diversity Requirements

Concept: Require proposals to exhibit "natural" diversity patterns.

Implementation:

type DiversityRequirements struct {
    MinRelays      int           // Must use >= N relays
    MinTimeJitter  time.Duration // Registrations can't be exactly spaced
    MaxSimilarity  float64       // Names can't be too similar (Levenshtein distance)
}

func (r *RegistryService) validateDiversity(npub string, reqs DiversityRequirements) error {
    proposals := r.getProposalsByAuthor(npub)

    // Check relay diversity
    relays := make(map[string]bool)
    for _, p := range proposals {
        relays[p.SeenOnRelay] = true
    }
    if len(relays) < reqs.MinRelays {
        return fmt.Errorf("must use %d different relays", reqs.MinRelays)
    }

    // Check timestamp jitter
    if len(proposals) > 1 {
        intervals := make([]time.Duration, len(proposals)-1)
        for i := 1; i < len(proposals); i++ {
            intervals[i-1] = proposals[i].CreatedAt.Sub(proposals[i-1].CreatedAt)
        }

        // If all intervals are suspiciously similar (< 10% variance), reject
        variance := calculateVariance(intervals)
        avgInterval := calculateAverage(intervals)
        if variance/avgInterval < 0.1 {
            return fmt.Errorf("timestamps too regular, appears automated")
        }
    }

    // Check name similarity
    for i := 0; i < len(proposals); i++ {
        for j := i + 1; j < len(proposals); j++ {
            similarity := levenshteinSimilarity(proposals[i].Name, proposals[j].Name)
            if similarity > reqs.MaxSimilarity {
                return fmt.Errorf("names too similar: %s and %s",
                    proposals[i].Name, proposals[j].Name)
            }
        }
    }

    return nil
}

Advantages:

  • Natural requirement for humans
  • Hard for bots to fake convincingly
  • Doesn't require state or external data

Disadvantages:

  • May flag legitimate bulk registrations
  • Requires careful threshold tuning
  • Can be bypassed with sufficient effort

6. Hybrid Approaches

6.1 Graduated Trust Model

Concept: Combine multiple mechanisms with progressive unlock.

Level 0 (New User):
- Account must be 7 days old
- Must have 10 events published
- Can register 1 name every 30 days
- 24-hour proposal delay
- Requires 2 endorsements

Level 1 (Established User):
- Account must be 90 days old
- Must have 100 events, 10 followers
- Can register 3 names every 30 days
- 6-hour proposal delay
- Requires 1 endorsement

Level 2 (Trusted User):
- Account must be 365 days old
- Must have 1000 events, 50 followers
- Can register 10 names every 30 days
- 1-hour proposal delay
- No endorsement required

Level 3 (Name Holder):
- Already holds an active name
- Can register unlimited subdomains under owned names
- Can register 5 TLDs every 30 days
- Instant proposal for subdomains
- Can vouch for others

Implementation:

type UserLevel struct {
    Level          int
    Requirements   Requirements
    Privileges     Privileges
}

type Requirements struct {
    MinAccountAge   time.Duration
    MinEvents       int
    MinFollowers    int
    MinActiveNames  int
}

type Privileges struct {
    MaxNamesPerPeriod int
    ProposalDelay     time.Duration
    EndorsementsReq   int
    CanVouch          bool
}

func (r *RegistryService) getUserLevel(npub string) UserLevel {
    age := r.getAccountAge(npub)
    events := r.getEventCount(npub)
    followers := r.getFollowerCount(npub)
    names := r.getActiveNameCount(npub)

    // Check Level 3
    if names > 0 {
        return UserLevel{
            Level: 3,
            Privileges: Privileges{
                MaxNamesPerPeriod: 5,
                ProposalDelay:     0,
                EndorsementsReq:   0,
                CanVouch:          true,
            },
        }
    }

    // Check Level 2
    if age >= 365*24*time.Hour && events >= 1000 && followers >= 50 {
        return UserLevel{
            Level: 2,
            Privileges: Privileges{
                MaxNamesPerPeriod: 10,
                ProposalDelay:     1 * time.Hour,
                EndorsementsReq:   0,
                CanVouch:          false,
            },
        }
    }

    // Check Level 1
    if age >= 90*24*time.Hour && events >= 100 && followers >= 10 {
        return UserLevel{
            Level: 1,
            Privileges: Privileges{
                MaxNamesPerPeriod: 3,
                ProposalDelay:     6 * time.Hour,
                EndorsementsReq:   1,
                CanVouch:          false,
            },
        }
    }

    // Default: Level 0
    return UserLevel{
        Level: 0,
        Privileges: Privileges{
            MaxNamesPerPeriod: 1,
            ProposalDelay:     24 * time.Hour,
            EndorsementsReq:   2,
            CanVouch:          false,
        },
    }
}

Advantages:

  • Flexible and granular
  • Rewards participation without hard barriers
  • Self-regulating (community grows trust over time)
  • Discourages throwaway accounts

Disadvantages:

  • Complex to implement and explain
  • May still be gamed by determined attackers
  • Requires careful balance of thresholds

For FIND, I recommend combining these mechanisms:

Base Layer: Time + WoT

type BaseRequirements struct {
    // Minimum account requirements
    MinAccountAge       time.Duration  // 30 days
    MinPublishedEvents  int            // 20 events
    MinEventKinds       []int          // Must have kind 1 (notes)

    // WoT requirements
    MinWoTScore         float64        // 0.01 (very low threshold)
    MinTrustedFollowers int            // 2 followers from trusted accounts

    // Proposal timing
    ProposalDelay       time.Duration  // 6 hours
}

Rate Limiting Layer: Progressive Cooldowns

type RateLimits struct {
    // First name: 7 day cooldown after
    // Second name: 14 day cooldown
    // Third name: 30 day cooldown
    // Fourth+: 60 day cooldown

    GetCooldown func(registrationCount int) time.Duration
}

Reputation Layer: Graduated Trust

// Users with existing names get faster registration
// Users with high WoT scores get reduced delays
// Users with endorsements bypass some checks

Detection Layer: Behavioral Analysis

// Flag suspicious patterns
// Require manual review for flagged accounts
// Share blocklists between registry services

This hybrid approach:

  • Low barrier for new legitimate users (30 days + minimal activity)
  • Strong sybil resistance (WoT + account age)
  • Prevents rapid squatting (progressive cooldowns)
  • Rewards participation (graduated trust)
  • Catches automation (behavioral analysis)
  • No monetary cost
  • No proof of work
  • Decentralized (no central authority)

8. Comparison Matrix

Mechanism Sybil Resistance UX Impact Implementation Complexity Bypass Difficulty
Proposal Delay Low High Low Low
Per-Account Cooldown Medium Medium Low Low (multiple keys)
Account Age Medium Low Low Medium (pre-age accounts)
Follow Count High Medium Medium High (requires real follows)
Endorsement System High High High High (requires cooperation)
Activity History High Low Medium High (must fake real activity)
Multi-Phase Commit Medium High Medium Medium (can automate)
Lottery System Medium Medium High Medium (sybil can spam proposals)
Queue/Priority High Low High High (merit-based)
Behavioral Analysis High Low Very High Very High (adaptive)
Hybrid Graduated Very High Medium High Very High

9. Attack Scenarios and Mitigations

Scenario 1: Sybil Attack (1000 throwaway npubs)

Mitigation: Account age + activity requirements filter out new accounts. WoT requirements prevent isolated accounts from registering.

Scenario 2: Pre-Aged Accounts

Attacker creates accounts months in advance Mitigation: Activity history requirements force ongoing engagement. Behavioral analysis detects coordinated registration waves.

Scenario 3: Follow-for-Follow Rings

Attackers create mutual follow networks Mitigation: WoT decay for insular networks. Only follows from trusted/bootstrapped accounts count.

Scenario 4: Bulk Registration by Legitimate User

Company wants 100 names for project Mitigation: Manual exception process for verified organizations. Higher-level users get higher quotas.

Scenario 5: Frontrunning

Attacker monitors proposals and submits competing proposal Mitigation: Proposal delay + lottery system makes frontrunning less effective. Random selection among competing proposals.


10. Configuration Recommendations

// Conservative (strict anti-spam)
conservative := RateLimitConfig{
    MinAccountAge:       90 * 24 * time.Hour,  // 90 days
    MinEvents:           100,
    MinFollowers:        10,
    ProposalDelay:       24 * time.Hour,
    CooldownPeriod:      30 * 24 * time.Hour,
    MaxNamesPerAccount:  5,
}

// Balanced (recommended for most relays)
balanced := RateLimitConfig{
    MinAccountAge:       30 * 24 * time.Hour,  // 30 days
    MinEvents:           20,
    MinFollowers:        2,
    ProposalDelay:       6 * time.Hour,
    CooldownPeriod:      7 * 24 * time.Hour,
    MaxNamesPerAccount:  10,
}

// Permissive (community trust-based)
permissive := RateLimitConfig{
    MinAccountAge:       7 * 24 * time.Hour,   // 7 days
    MinEvents:           5,
    MinFollowers:        0,  // No WoT requirement
    ProposalDelay:       1 * time.Hour,
    CooldownPeriod:      24 * time.Hour,
    MaxNamesPerAccount:  20,
}

Each relay can choose their own configuration based on their community values and spam tolerance.


Conclusion

Non-monetary, non-PoW rate limiting is achievable through careful combination of:

  1. Time-based friction (delays, cooldowns)
  2. Social proof (WoT, endorsements)
  3. Behavioral signals (activity history, pattern detection)
  4. Graduated trust (reward long-term participation)

The key insight is that time + social capital can be as effective as monetary deposits for spam prevention, while being more aligned with Nostr's values of openness and decentralization.

The recommended hybrid approach provides strong sybil resistance while maintaining accessibility for legitimate new users, creating a natural barrier that's low for humans but high for bots.