This is a future profile. nostr-veil can already attach an anonymous threshold-backed score to a credential or attestation event, but it does not yet define the credential format, endorsement semantics, expiry, revocation, or presentation rules.
Fit
- Status: future profile, with a supported building block today.
- Current NIP-85 shape: kind 30383 for an attestation event id, or 30384 for an addressable credential record.
- Subject: the credential or attestation event, not the holder in general.
- Helpers today:
contributeEventAssertionorcontributeAddressableAssertion. - Proof version: v2 strongly recommended.
- Useful metric today:
rankas endorsement confidence.
Subject design
- Today, score the credential or attestation event, not the holder in general.
- Use kind 30383 when the attestation is a fixed event id.
- Use kind 30384 when the credential record is addressable and the profile defines how revisions, expiry, and revocation work.
- Do not use a generic holder pubkey score as a credential. Holder binding, presentation, and selective disclosure are separate protocol work.
What to publish
- Today: a normal event or addressable assertion that says an attestor circle endorsed a specific credential artefact.
- A
rankprofile such as endorsement confidence, issuer-confidence score, or review completeness. - Proof v2 tags, accepted attestor-circle policy, threshold, expiry, and the credential class being reviewed.
- Future profile data for holder binding, presentation challenge, disclosed attributes, revocation lookup, and verifier discovery.
Implementation recipe for today's building block
- Publish or identify the credential or attestation event to be scored.
- Have the attestor circle publish a proof v2 event or addressable assertion against that exact subject.
- Verify the expected subject, circle, threshold, and score before treating the attestation as endorsed.
- Keep holder binding, presentation, selective disclosure, expiry, and revocation in the credential profile until nostr-veil has native support for those semantics.
Worked example for today's building block
import {
NIP85_KINDS,
aggregateEventContributions,
contributeEventAssertion,
createTrustCircle,
} from 'nostr-veil'
import {
defaultMembers,
memberIndex,
proofVersion,
verifyUseCaseAssertion,
withCreatedAt,
} from './_shared.js'
const slug = 'anonymous-credential-attestation-cosigning'
const attestationEventId = 'cc'.repeat(32)
const circle = createTrustCircle(defaultMembers.map(member => member.pub))
const contributions = defaultMembers.map((attestor, index) =>
contributeEventAssertion(
circle,
attestationEventId,
{ rank: 90 + index },
attestor.priv,
memberIndex(circle, attestor.pub),
{ proofVersion },
),
)
export const assertion = withCreatedAt(aggregateEventContributions(
circle,
attestationEventId,
contributions,
{ proofVersion },
))
export const result = verifyUseCaseAssertion(slug, assertion, {
kind: NIP85_KINDS.EVENT,
subject: attestationEventId,
subjectTag: 'e',
circleId: circle.circleId,
minDistinctSigners: 3,
freshAfter: assertion.created_at - 300,
})
What to verify
- Today: strict syntax and a valid proof v2 for the attestation event or addressable credential record.
- The subject tag points to the exact credential artefact being considered.
- The attestor circle is accepted for that credential class and has enough distinct signers.
- The credential profile supplies holder binding, challenge/response, revocation, expiry, and disclosure checks before the verifier treats the presenter as credentialed.
- The assertion has not been superseded by revocation or a later credential revision.
What this proves today
- A circle scored a specific attestation event.
- Distinct members contributed, without being named.
- The aggregate score is bound to the event id.
What a full profile still needs
- A credential or attestation event format.
- Holder binding and presentation rules.
- Expiry and revocation semantics.
- Whether endorsements are binary, ranked, weighted, or scoped.
- How a verifier discovers the right circle for the credential class.
What not to claim
- Do not claim nostr-veil currently issues anonymous credentials. It can score or endorse a credential artefact today.
- Do not claim a scored credential proves the presenter is the holder without a holder-binding and presentation protocol.
- Do not claim selective disclosure, revocation, or expiry semantics exist until a credential profile defines them.
Failure handling
- Reject credential endorsements that point at the wrong artefact, unknown attestor circles, stale records, or unsupported credential classes.
- Treat missing revocation or holder-binding checks as a verifier failure, not as a weak warning.
- Publish superseding endorsement assertions when an attestation is corrected, revoked, or replaced.
- Keep personal attributes out of the nostr-veil assertion unless the credential profile explicitly and safely discloses them.
Operational requirements
| Risk to handle | Required control |
|---|---|
| nostr-veil does not currently issue anonymous credentials. | Define a credential or attestation event format, issuer rules, holder binding, and presentation flow. |
| Selective disclosure is not defined here. | Add a credential protocol that can reveal only the required attributes while keeping the nostr-veil proof as the anonymous endorsement signal. |
| The proof does not prove the credential subject is the presenter. | Add holder-binding keys, challenge/response presentation, and replay protection. |
| Revocation is not defined yet. | Add expiry, revocation events, revocation discovery, and client policy for stale credentials. |
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
c300590f1a6f...904591d4
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.