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, thenverifyFederation. - 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
scopebefore 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-scopevalue 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
- Agree the federation
scopebefore any circle publishes assertions. - Use the same subject and assertion kind across all participating circles.
- Verify each event independently, then call
verifyFederationto count scoped key images once. - Decide whether every circle must meet its own threshold before the federation-level count is accepted.
- Use isolated circles instead of a shared scope if cross-circle overlap would create unacceptable privacy risk.
Worked example
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).validis true, with one shared subject and one shared scope.- Each participating circle is accepted by the federation policy.
- The federation-level
distinctSignerscount 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 handle | Required 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.
Nostr pubkey subjects. Subject hint: p.
Nostr event id subjects. Subject hint: e.
NIP-33 address subjects. Subject hint: a.
packages, relays, domains, vendors, and other identifiers. Subject hint: k.
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.
- 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.
Published scores must still match the signed contribution aggregate.
The d tag and subject hint must stay bound to the signed v2 proof.
The assertion kind must match the profile and the signed v2 context.
New deployment profiles require proof v2.
Repeated key images must not increase the signer count.
Removing a signature must fail the profile threshold.
created_at must remain inside the freshness window.
The circle ID must be accepted by deployment policy.
verifyProductionDeployment() should require a signed deployment bundle from a trusted publisher.
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.