People

Privacy-preserving onboarding

Use this when an existing circle wants to vouch that a new account passed an admission or onboarding policy without naming which members vouched.

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: rank as 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 d and p; 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 rank value 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

  1. Define the admission policy and the threshold needed before a vouch is useful.
  2. Publish a kind 30382 assertion about the candidate pubkey with proof v2.
  3. Verify the expected circle, threshold, candidate subject, and score before applying community policy.
  4. Add expiry and revocation policy so old vouches do not become permanent social credentials.
  5. For anonymous gated access, pair this with the future admission handshake described in relay or community admission.

Worked example

examples/use-cases/privacy-preserving-onboarding.ts
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 d and p equal to the candidate pubkey being considered.
  • The veil-ring belongs to an accepted admission circle and proof.distinctSigners meets the community threshold.
  • The rank meaning 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 handleRequired 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.

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
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.

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