Use this when the subject is a Nostr account and the circle wants to publish a trust or abuse-risk signal without exposing which members contributed.
Fit
- Status: supported today.
- NIP-85 kind: 30382 user assertion.
- Subject: account pubkey in the
dtag, optionally mirrored inp. - Helpers:
contributeAssertion,aggregateContributions. - Proof version: v2 recommended for new deployments, v1 remains compatible.
- Useful metrics:
rank,reports_cnt_recd,reports_cnt_sent, and other supported user metrics.
Define metric direction before publishing. A practical profile is:
- high
rank: trusted or low risk; - low
rank: low trust or higher concern; reports_cnt_recd: number of report-like contributions represented by the anonymous reviewers.
Subject design
- Use this profile only when the action target is a Nostr pubkey.
- Put the same pubkey in
dandp; proof v2 binds the signed contribution to that user-assertion subject. - Do not mix identity confidence, abuse severity, and report volume into one undocumented number. If clients need all three, publish separate assertions or define a profile that explains the weighting.
- Treat the public
veil-ringas the reviewer cover set. It should be large enough that membership does not identify victims, witnesses, or moderators.
What to publish
- A kind 30382 assertion created with
aggregateContributions. - Metric tags whose direction is documented before collection, usually
rankand optionallyreports_cnt_recd. veil-ring,veil-threshold,veil-agg,veil-version, andveil-sigtags from the aggregator.- A separate policy page or provider profile that tells clients which circle, threshold, evidence standard, expiry, appeal path, and recovery rules apply.
Implementation recipe
- Publish the circle admission policy before accepting reports.
- Define whether
rankmeasures trust, risk, confidence, or severity. - Batch or delay contribution collection if timing could identify reviewers.
- Aggregate with proof v2 and publish the resulting kind 30382 event.
- On the client, require strict syntax, a valid proof, the expected circle ID, and a policy threshold before taking action.
Worked example
import {
NIP85_KINDS,
aggregateContributions,
contributeAssertion,
createTrustCircle,
} from 'nostr-veil'
import {
defaultMembers,
memberIndex,
proofVersion,
subjectPubkey,
verifyUseCaseAssertion,
withCreatedAt,
} from './_shared.js'
const slug = 'user-reputation-abuse-reporting'
const circle = createTrustCircle(defaultMembers.map(member => member.pub))
const contributions = defaultMembers.map((reviewer, index) =>
contributeAssertion(
circle,
subjectPubkey,
{
rank: 24 + index * 3,
reports_cnt_recd: index + 1,
},
reviewer.priv,
memberIndex(circle, reviewer.pub),
{ proofVersion },
),
)
export const assertion = withCreatedAt(aggregateContributions(
circle,
subjectPubkey,
contributions,
{ proofVersion },
))
export const result = verifyUseCaseAssertion(slug, assertion, {
kind: NIP85_KINDS.USER,
subject: subjectPubkey,
subjectTag: 'p',
circleId: circle.circleId,
minDistinctSigners: 3,
freshAfter: assertion.created_at - 300,
})
The output is a kind 30382 event with d, p, metric tags, and veil-* proof tags. A verifier can see the aggregate score and that distinct circle members signed it; they cannot see which members signed.
What to verify
validateAssertionStrict(assertion).validis true.verifyProof(assertion, { requireProofVersion: 'v2' }).validis true.- The event is kind 30382 and both
dandpequal the user pubkey the client is about to act on. - The
veil-ringmatches an accepted moderation circle, or its computed circle ID is in the client's allow-list. proof.distinctSignersmeets the policy threshold, therankdirection is known, and the assertion is fresh enough for the action being taken.
What this proves
- The event is syntactically a strict user assertion.
- Each contribution was signed by some member of the published ring.
- The same member did not contribute twice to this circle/scope and subject.
- The metric tags match the aggregate of the signed contributions.
- With proof v2, the proof is bound to kind 30382 and the
psubject hint.
What not to claim
- Do not claim the proof proves abuse happened. It proves a threshold-backed reviewer signal about the pubkey.
- Do not claim reviewers are invisible to all observers. The public ring shows who could have contributed; timing, relay, and collector metadata still need operational controls.
- Do not claim the circle is inherently fair or Sybil-resistant. That is a governance property of the circle, not a cryptographic property of the proof.
Failure handling
- Reject malformed proofs, wrong-subject assertions, unknown circles, stale events, and scores whose metric direction is not published.
- Route low-score or high-severity outcomes into the moderation process instead of treating the score as the whole case file.
- Publish correction or recovery assertions when an account is cleared, compromised, restored, or re-reviewed.
- Keep appeal evidence outside the nostr-veil event and link only the public decision state that clients need.
Operational requirements
| Risk to handle | Required control |
|---|---|
| The proof does not prove that abuse happened. | Keep a separate evidence workflow, retention policy, escalation path, and appeal process. Use the nostr-veil score as the anonymous threshold signal, not as the whole case file. |
| The proof does not prove the circle is fair, independent, or Sybil-resistant. | Publish admission criteria, rotate compromised members, use independent circles for high-impact actions, and audit circle membership changes. |
| The public ring reveals who could have contributed. | Use a cover set large enough for the risk, avoid circles made only of vulnerable reporters, and separate "reviewer circle" membership from "victim or witness" identity. |
| Network, timing, or collector metadata can still leak. | Batch collection, avoid one-to-one submission timing, use transport privacy where appropriate, and avoid logging contributor IPs or relay metadata. |
Policy choices
- Who can join the moderation or trust circle?
- How are false reports handled?
- Does a low score expire or recover over time?
- What threshold is enough for client action?
- Who signs and publishes the final aggregate event?
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
- 1/1 fetched from relay
- Proof
- 3/3 threshold from a 3-member ring
- 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
aeabeeb71083...0954a15e
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.