Governance

Federated moderation

Use this when several trust circles assess the same subject and the verifier needs to count distinct contributors across circles without double-counting members who appear in more than one circle.

Use this when several trust circles assess the same subject and the verifier needs to count distinct contributors across circles without double-counting members who appear in more than one circle.

Fit

  • Status: supported today.
  • NIP-85 kind: any supported assertion kind, as long as all events agree on the same subject and scope.
  • Subject: usually a user pubkey or event id.
  • Helpers: createTrustCircle(members, { scope }), aggregate normally, then verifyFederation.
  • Scope format: lowercase slug using letters, digits, dot, hyphen, or underscore.
  • Proof version: v2 recommended.

Subject design

  • Use federation when several circles score the same subject and a verifier must deduplicate contributors who are members of more than one circle.
  • Create each circle with the same scope before collecting contributions. Scoped key images are what make cross-circle deduplication possible.
  • Keep the subject route identical across events. Do not federate a user assertion with an event assertion or an identifier assertion.
  • Use unscoped circles when overlap privacy matters more than deduplication.

What to publish

  • One normal nostr-veil assertion per circle, all for the same subject and assertion kind.
  • A shared veil-scope value emitted by circles created with that scope.
  • A federation policy explaining participating circles, minimum per-circle thresholds, overall distinct-signer threshold, weighting, and disagreement handling.
  • No attempt to merge raw signatures into one event; verifiers should verify the separate events and then call verifyFederation.

Implementation recipe

  1. Agree the federation scope before any circle publishes assertions.
  2. Use the same subject and assertion kind across all participating circles.
  3. Verify each event independently, then call verifyFederation to count scoped key images once.
  4. Decide whether every circle must meet its own threshold before the federation-level count is accepted.
  5. Use isolated circles instead of a shared scope if cross-circle overlap would create unacceptable privacy risk.

Worked example

examples/use-cases/federated-moderation.ts
import {
  NIP85_KINDS,
  aggregateContributions,
  contributeAssertion,
  createTrustCircle,
} from 'nostr-veil'
import {
  keys,
  memberIndex,
  proofVersion,
  subjectPubkey,
  verifyFederatedUseCase,
  withCreatedAt,
} from './_shared.js'
import type { TrustCircle } from 'nostr-veil'

const slug = 'federated-moderation'
const scope = 'moderation.federation.example'
const circleA = createTrustCircle(keys.slice(0, 3).map(member => member.pub), { scope })
const circleB = createTrustCircle(keys.slice(1, 4).map(member => member.pub), { scope })

function makeAssertion(circle: TrustCircle, members: typeof keys, offset: number) {
  const contributions = members.map((member, index) =>
    contributeAssertion(
      circle,
      subjectPubkey,
      {
        rank: offset + index * 5,
        reports_cnt_recd: index + 1,
      },
      member.priv,
      memberIndex(circle, member.pub),
      { proofVersion },
    ),
  )

  return withCreatedAt(aggregateContributions(
    circle,
    subjectPubkey,
    contributions,
    { proofVersion },
  ))
}

export const events = [
  makeAssertion(circleA, keys.slice(0, 3), 30),
  makeAssertion(circleB, keys.slice(1, 4), 35),
]

export const result = verifyFederatedUseCase(slug, events, {
  kind: NIP85_KINDS.USER,
  subject: subjectPubkey,
  scope,
  minDistinctSigners: 4,
})

What to verify

  • Each event verifies independently with verifyProof, using proof v2 for new deployments.
  • verifyFederation(events).valid is true, with one shared subject and one shared scope.
  • Each participating circle is accepted by the federation policy.
  • The federation-level distinctSigners count and any per-circle thresholds meet the action threshold.
  • Clients understand that repeated scoped key images reveal cross-circle membership overlap for an otherwise anonymous contributor.

What this proves

  • Each event independently verifies.
  • All events agree on subject and scope.
  • Matching scoped key images are counted once across circles.
  • A member who contributed in multiple circles does not inflate the total.

What not to claim

  • Do not claim scoped federation proves the circles are independent. It only deduplicates overlapping contributors.
  • Do not claim unscoped events can be deduplicated later. The scope must be part of contribution creation.
  • Do not claim overlap is private. Shared scoped key images intentionally reveal that the same unknown member contributed in more than one circle.

Failure handling

  • Reject federations with mixed subjects, mixed scopes, mixed assertion kinds, unknown circles, or invalid per-event proofs.
  • Fall back to showing separate circle results when overlap privacy is more important than deduplication.
  • Escalate disagreement between circles according to the federation policy rather than averaging away a meaningful split.
  • Rotate or remove captured circles through governance; the proof layer cannot repair bad federation membership.

Operational requirements

Risk to handleRequired control
Scoped federation reveals that the same unknown contributor appeared in more than one circle.Use scoped federation only when deduplication is worth that overlap signal. Otherwise keep circles unscoped and display separate circle results.
The proof does not prove circles are independent.Publish federation membership rules, circle admission policies, and governance for conflicts or captured circles.
Unscoped events cannot be deduplicated across circles.Create circles with the same scope before collecting contributions; do not try to retrofit deduplication onto isolated events.
Different circles may disagree.Define client policy for thresholds, weighting, abstentions, and whether disagreement blocks action or lowers confidence.

Policy choices

  • Is revealing cross-circle overlap acceptable for this federation?
  • Which communities are allowed to share the scope?
  • Does the federation require every circle to meet its own threshold first?
  • How should clients display disagreement between circles?

NIP-85 kind reference

NIP-85 defines the assertion kind by the subject being scored. The kind number is part of the proof v2 context, so deployments should verify both the number and the subject hint tag.

30382 User assertion

Nostr pubkey subjects. Subject hint: p.

30383 Event assertion

Nostr event id subjects. Subject hint: e.

30384 Addressable event assertion

NIP-33 address subjects. Subject hint: a.

30385 NIP-73/external identifier assertion

packages, relays, domains, vendors, and other identifiers. Subject hint: k.

10040 Trusted service provider declaration

provider metadata, not a score assertion. Subject hint: provider tags.

Spec: NIP-85 trusted assertions.

Live relay test

The opt-in relay test signs this canonical example as real Nostr event data, publishes it to wss://relay.trotters.cc, fetches it back by id, and re-runs the application, syntax, Nostr signature, canonical tag, and proof checks.

Passed 2026-05-17T21:54:12.000Z
Events
2/2 fetched from relay
Proof
4 distinct signers across 2 circles
Run
tf7bic-3b3a5ae6d6
  • Canonical example passes locally
  • Relay stored and returned every signed event
  • Fetched Nostr event signatures are valid
  • Fetched tags match the canonical example
  • NIP-85 syntax validation passes
  • nostr-veil proof verification passes
  • Deployment profile verifier passes
1b1043fc8803...6e1b0156 3af86a6c8763...8a4dee12

Run the same check with npm run test:use-cases:relay -- --write docs/use-case-relay-checks.json.

Safety checks

Each canonical use-case example is also exercised by an adversarial test harness. These are the failure modes a production verifier should reject before acting on the score.

Tampered metric

Published scores must still match the signed contribution aggregate.

Wrong subject

The d tag and subject hint must stay bound to the signed v2 proof.

Wrong kind

The assertion kind must match the profile and the signed v2 context.

Proof downgrade

New deployment profiles require proof v2.

Duplicate signer

Repeated key images must not increase the signer count.

Insufficient threshold

Removing a signature must fail the profile threshold.

Stale assertion

created_at must remain inside the freshness window.

Unknown circle

The circle ID must be accepted by deployment policy.

Unsigned policy

verifyProductionDeployment() should require a signed deployment bundle from a trusted publisher.

Relay mutation

Fetched event content and tags must match the Nostr signature.

Use validateUseCaseProfileDefinition() for custom profiles, then verifyProductionDeployment() with trusted bundle publishers, signed relay events, accepted circle manifests, expected subject, freshness, and threshold policy so these checks are not left to application glue. For application UI and audit logs, use verifyProductionDeploymentReport() or createProductionDecisionReport() so failures include issue codes, remediation text, a recommended action, pass/fail/not-checked status for the controls, and the profile's proofClaims, proofLimitations, requiredControls, and recommendedActions.

Next examples