One Nostr event kind for all attestations -- credentials, endorsements, vouches, provenance, licensing, and trust. Supports both direct attestation and assertion-first patterns within a single kind.
Nostr: npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2
npm install nostr-attestations
import { createAttestation, TYPES } from 'nostr-attestations'
// Create an unsigned attestation event template
const event = createAttestation({
type: TYPES.CREDENTIAL,
identifier: '<subject-pubkey>',
subject: '<subject-pubkey>',
summary: 'Professional credential verified',
expiration: 1735689600,
tags: [['profession', 'attorney'], ['jurisdiction', 'US-NY']],
content: JSON.stringify({ proof: '...' }),
})
// => { kind: 31000, tags: [...], content: '...' }
// Sign with your preferred library and publish to relaysNo relay client, no signing library, no crypto. Bring your own. Works with nostr-tools, any Nostr SDK, or bare WebSocket code.
Attest to someone else's claim — the individual makes an assertion, you verify it:
import { createAttestation } from 'nostr-attestations'
// Attest to a first-person assertion event
const event = createAttestation({
assertion: { id: '<assertion-event-id>', relay: 'wss://relay.example.com' },
subject: '<subject-pubkey>',
content: 'Identity verified in person',
})
// type is inferred from the referenced assertion — no type tag neededRecord when the attested event actually happened, separate from when the attestation was published:
const event = createAttestation({
type: TYPES.ENDORSEMENT,
subject: '<service-provider-pubkey>',
occurredAt: 1710900000, // service completed last Tuesday
summary: 'Completed frontend work ahead of schedule',
})
// created_at = when attestation published, occurred_at = when it happenedAll attestations include NIP-32 labels for relay-side discoverability. Typed attestations also include a type label:
["L", "nip-va"]
["l", "credential", "nip-va"]This enables queries like {"#L": ["nip-va"]} (all attestations) or {"#l": ["credential"]} (by type).
Nostr has several ways to label or badge identities, but none designed for verifiable attestations. NIP-58 badges are display-only — no expiry, no revocation, no structured claims. NIP-85 covers social graph metrics, not arbitrary claims. NIP-32 labels are lightweight but not individually replaceable per subject.
nostr-attestations uses one kind (31000) with a type tag instead of inventing a new event kind for every attestation use case. Credentials, endorsements, vouches, licensing, provenance, and revocations all share the same event structure. New attestation types need zero protocol changes — just define a new type value.
It supports two attestation patterns within the same kind:
- Direct attestation — the attestor defines the type and makes a claim about a subject
- Assertion-first — the subject makes their own claim, and the attestor references and verifies it
The base layer is semantically neutral — it carries attestations but does not interpret them. Application profiles (identity verification, professional licensing, service reputation) are built downstream, not in the library.
import { createRevocation, isRevoked, parseAttestation } from 'nostr-attestations'
// Revoke a previously issued attestation
const revocation = createRevocation({
type: 'credential',
identifier: '<subject-pubkey>',
subject: '<subject-pubkey>',
reason: 'licence-expired',
effective: 1704067200,
})
// Publish — addressable event semantics replace the original
// Check if a fetched event is revoked
const revoked = isRevoked(event) // true if ["status", "revoked"] tag presentimport { parseAttestation } from 'nostr-attestations'
const attestation = parseAttestation(event) // returns null for non-attestation events
if (!attestation) throw new Error('Not a valid attestation')
// {
// kind: 31000,
// type: 'credential', // "assertion" for assertion-only attestations
// pubkey: '<attestor-pubkey>',
// createdAt: 1700000000,
// identifier: '<subject-pubkey>',
// subject: '<subject-pubkey>',
// assertionId: null, // event ID if assertion-first
// assertionAddress: null, // addressable coord if assertion-first
// assertionRelay: null, // relay hint for assertion
// summary: 'Professional credential verified',
// expiration: 1735689600,
// validFrom: null,
// validTo: null, // validity window end
// request: null, // what prompted this attestation
// schema: null, // machine-readable schema URI
// occurredAt: null, // when the attested event happened
// revoked: false,
// reason: null,
// tags: [...],
// content: '...',
// }| Function | Signature | Returns |
|---|---|---|
createAttestation |
(params: AttestationParams) => EventTemplate |
Unsigned attestation event |
createRevocation |
(params: RevocationParams) => EventTemplate |
Unsigned revocation event |
| Function | Signature | Returns |
|---|---|---|
parseAttestation |
(event: NostrEvent) => Attestation | null |
Typed attestation data, or null for non-attestation events |
isRevoked |
(event: NostrEvent) => boolean |
True if event has ["status", "revoked"] |
| Function | Signature | Returns |
|---|---|---|
validateAttestation |
(event: NostrEvent) => ValidationResult |
{ valid: boolean, errors: string[] } |
| Function | Signature | Returns |
|---|---|---|
isValid |
(event: NostrEvent, now?: number) => ValidityResult |
{ valid: boolean, reason?: string } |
| Function | Signature | Returns |
|---|---|---|
attestationFilter |
(params: FilterParams) => NostrFilter |
Relay query filter |
revocationFilter |
(type, identifier) or ({ assertionId | assertionAddress }) |
Revocation check filter |
buildDTag |
(type: string, identifier: string) => string |
"type:identifier" string |
buildAssertionDTag |
(ref: string) => string |
"assertion:ref" string |
parseDTag |
(dTag: string) => { type: string; identifier: string } | null |
Parsed d-tag (type is "assertion" for assertion-only) |
| Export | Value | Description |
|---|---|---|
ATTESTATION_KIND |
31000 |
NIP-VA event kind |
TYPES |
{ CREDENTIAL, ENDORSEMENT, VOUCH, VERIFIER, PROVENANCE } |
Well-known type constants |
AssertionRef, AttestationParams, RevocationParams, Attestation, ValidationResult, ValidityResult, FilterParams, NostrFilter, NostrEvent, EventTemplate — all exported from the package root and from nostr-attestations/types (zero-runtime import).
vectors/attestations.json contains 20 frozen conformance test vectors covering the full range of attestation types (credential, endorsement, vouch, verifier, provenance) and states (active, revoked, self-attestation). Any conformant implementation must produce identical parse results from these inputs. The vectors are pinned — if tests against them fail, the implementation is broken, not the vector.
This library's authorship is claimed on Nostr using the very protocol it implements — NIP-VA eating its own dog food.
A self-attestation alone only proves that the holder of a private key claims authorship — not that the claim is true. The real value comes from third-party attestations: other pubkeys independently publishing type: endorsement events that reference this repo.
But counting endorsements isn't enough either — anyone can create 50 throwaway npubs and endorse themselves. What matters is who endorses, not how many. A single endorsement from a pubkey with a verified NIP-05 domain, a history of notes, and followers you recognise is worth more than a thousand from anonymous keys. This is a web of trust, not a vote count. When verifying, ask: do I know this endorser? Do people I trust follow them? That's how you resist Sybil attacks without a centralised authority.
All verification uses nak (the Nostr Army Knife). Install with go install github.com/fiatjaf/nak@latest or brew install fiatjaf/tap/nak.
1. GitHub → Nostr — this README claims npub1mgv... (see header above)
2. Nostr → GitHub — the repo announcement points back here:
nak req -k 30617 \
-a $(nak decode npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2) \
-d nostr-attestations \
wss://relay.damus.io
# Look for the "web" tag → github.com/forgesworn/nostr-attestations3. Verify the authorship claim — signed by the same key:
nak req -k 31000 \
-a $(nak decode npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2) \
-d authorship:nostr-attestations \
wss://relay.damus.io 2>/dev/null | nak verify \
&& echo "✓ Signature valid"4. Check for third-party endorsements:
nak req -k 31000 \
-t a=30617:da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd:nostr-attestations \
wss://relay.damus.io
# Returns all attestations referencing this repo — filter by pubkey or type client-sideSame npub on both sides — you'd need to control both GitHub and the private key to fake it. Third-party endorsements add independent signatures that can't be faked by one person.
Endorse it yourself:
nak event -k 31000 \
--prompt-sec \
-d endorsement:da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd \
-t type=endorsement \
-p da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd \
-t a=30617:da19f1cd34beca44be74da4b306d9d1dd86b6343cef94ce22c49c6f59816e5bd:nostr-attestations \
-t summary="Reviewed and endorsed nostr-attestations" \
wss://relay.damus.io wss://nos.lol wss://relay.nostr.bandFull protocol specification: NIP-VA.md | NostrHub
ForgeSworn builds open-source cryptographic identity, payments, and coordination tools for Nostr.
| Library | What it does |
|---|---|
| nsec-tree | Deterministic sub-identity derivation |
| ring-sig | SAG/LSAG ring signatures on secp256k1 |
| range-proof | Pedersen commitment range proofs |
| canary-kit | Coercion-resistant spoken verification |
| spoken-token | Human-speakable verification tokens |
| toll-booth | L402 payment middleware |
| geohash-kit | Geohash toolkit with polygon coverage |
| nostr-attestations | NIP-VA verifiable attestations |
| dominion | Epoch-based encrypted access control |
| nostr-veil | Privacy-preserving Web of Trust |
MIT