Use this when an existing circle wants to vouch that a new account passed an admission or onboarding policy without naming which members vouched.
Fit
- Status: supported today as a vouch assertion.
- NIP-85 kind: 30382 user assertion.
- Subject: candidate account pubkey.
- Helpers:
contributeAssertion,aggregateContributions. - Proof version: v2 recommended.
- Useful metrics:
rankas admission confidence or community standing.
This is not anonymous gated access by itself. It creates a verifiable vouch event that another client, relay, or community can use as an input to policy.
Subject design
- Use the candidate pubkey as the subject when the vouch should be portable across clients or communities.
- Put the candidate pubkey in both
dandp; proof v2 binds the vouch to that candidate, not to a future session or account-creation flow. - Keep onboarding vouches separate from later behaviour scores. A good admission signal should not permanently override moderation outcomes.
- If the candidate pubkey itself must stay private, this profile is not enough; use the future admission profile with a separate presentation handshake.
What to publish
- A kind 30382 assertion created with
aggregateContributions. - A
rankvalue with a clear meaning such as admission confidence, community fit, or verified-sponsor confidence. - Proof v2 tags and a public policy for accepted circles, minimum threshold, expiry, revocation, and re-review.
- No private application notes or onboarding evidence in the assertion content; keep those in the community's internal workflow.
Implementation recipe
- Define the admission policy and the threshold needed before a vouch is useful.
- Publish a kind 30382 assertion about the candidate pubkey with proof v2.
- Verify the expected circle, threshold, candidate subject, and score before applying community policy.
- Add expiry and revocation policy so old vouches do not become permanent social credentials.
- For anonymous gated access, pair this with the future admission handshake described in relay or community admission.
Worked example
import {
NIP85_KINDS,
aggregateContributions,
contributeAssertion,
createTrustCircle,
} from 'nostr-veil'
import {
defaultMembers,
memberIndex,
proofVersion,
subjectPubkey,
verifyUseCaseAssertion,
withCreatedAt,
} from './_shared.js'
const slug = 'privacy-preserving-onboarding'
const candidatePubkey = subjectPubkey
const circle = createTrustCircle(defaultMembers.map(member => member.pub))
const contributions = defaultMembers.map((member, index) =>
contributeAssertion(
circle,
candidatePubkey,
{ rank: 88 + index },
member.priv,
memberIndex(circle, member.pub),
{ proofVersion },
),
)
export const assertion = withCreatedAt(aggregateContributions(
circle,
candidatePubkey,
contributions,
{ proofVersion },
))
export const result = verifyUseCaseAssertion(slug, assertion, {
kind: NIP85_KINDS.USER,
subject: candidatePubkey,
subjectTag: 'p',
circleId: circle.circleId,
minDistinctSigners: 3,
freshAfter: assertion.created_at - 300,
})
What to verify
- Strict NIP-85 syntax and a valid proof v2.
- Kind 30382, with
dandpequal to the candidate pubkey being considered. - The
veil-ringbelongs to an accepted admission circle andproof.distinctSignersmeets the community threshold. - The
rankmeaning matches the community's onboarding policy. - The assertion is not expired or superseded by a revocation, ban, or later re-review.
What this proves
- Enough distinct circle members vouched for the candidate.
- The candidate subject is bound to the proof.
- No individual vouching member is identified.
What not to claim
- Do not claim the user was admitted anonymously. The candidate pubkey is public in the assertion.
- Do not claim the assertion grants relay access by itself. A relay or community still needs policy code that accepts or rejects the verified vouch.
- Do not claim a vouch predicts future behaviour. It is an admission signal at a point in time.
Failure handling
- Reject vouches from unknown circles, below-threshold circles, expired assertions, or assertions about the wrong pubkey.
- Expire onboarding vouches by default and require re-review for sensitive communities.
- Publish a revocation or updated assertion when a sponsor withdraws support or the candidate later violates policy.
- Fall back to manual review when independent circles disagree.
Operational requirements
| Risk to handle | Required control |
|---|---|
| The assertion does not admit the user to a relay by itself. | Implement relay or community policy that checks the assertion and then grants access. |
| The candidate pubkey is visible. | Use this for portable vouching today. For private membership or unlinkable entry, add a separate anonymous admission protocol. |
| The proof does not prevent later misbehaviour. | Add expiry, re-review, revocation, moderation policy, and post-admission enforcement. |
| A single circle may be captured or too local. | Require multiple independent circles or a scoped federation for higher-risk communities. |
Policy choices
- What threshold is enough to join?
- Should onboarding vouches expire?
- Can a vouch be revoked later?
- Does the candidate need multiple independent 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
- 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
04f895ca48a6...ff2f6b4a
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.