Infrastructure

NIP-05, domain, and service-provider trust

Use this when the subject is an identity domain, NIP-05 name, upload provider, payment endpoint, or other service identifier outside a single Nostr pubkey.

Use this when the subject is an identity domain, NIP-05 name, upload provider, payment endpoint, or other service identifier outside a single Nostr pubkey.

Fit

  • Status: supported today.
  • NIP-85 kind: 30385 identifier assertion.
  • Subject examples: nip05:alice@example.com, domain:example.com, lnurlp:alice@example.com, nip96:https://upload.example.com.
  • Canonical helpers: canonicalNip05Subject, canonicalDomainSubject, canonicalLnurlpSubject, canonicalNip96Subject, and canonicalServiceSubject.
  • Helpers: contributeIdentifierAssertion, aggregateIdentifierContributions.
  • Proof version: v2 recommended.
  • Useful metrics: rank.

Subject design

  • Use kind 30385 because the subject is an identifier outside one Nostr pubkey.
  • Decide whether the subject is a whole domain, a specific NIP-05 name, an LNURL/payment identifier, an upload endpoint, or a provider account.
  • Canonicalise host names, schemes, trailing slashes, punycode, and case rules before anyone signs.
  • Keep domain trust, NIP-05 name confidence, provider behaviour, and payment endpoint risk as separate subjects unless the profile deliberately combines them.

What to publish

  • A kind 30385 assertion with the canonical identifier in d and the profile namespace in k.
  • A rank profile explaining whether the score means identity confidence, domain-control confidence, provider reliability, abuse risk, or operational trust.
  • Proof v2 tags, accepted circle policy, threshold, freshness, and revocation or incident rules.
  • Evidence references only when safe; DNS, HTTPS, NIP-05, and payment checks should happen before review, not inside the nostr-veil proof.

Implementation recipe

  1. Canonicalise the identifier before signing with the NIP-05, domain, LNURLp, NIP-96, or service helper: lowercase host names, agreed schemes, punycode handling, and trailing slash rules.
  2. Decide whether the subject is a domain, a specific NIP-05 name, a payment endpoint, or a service provider.
  3. Run the underlying service check outside nostr-veil, then publish the threshold-backed assessment.
  4. Require proof v2 and verify the expected identifier string, k namespace, circle, threshold, and freshness.
  5. Use expiry because domain ownership and service behaviour can change. In production, put resolver and service checks in companionEvidence so stale or missing off-chain evidence rejects automatically.

Worked example

examples/use-cases/nip05-domain-service-provider-trust.ts
import {
  NIP85_KINDS,
  aggregateIdentifierContributions,
  contributeIdentifierAssertion,
  createTrustCircle,
} from 'nostr-veil'
import {
  defaultMembers,
  externalProfileKind,
  memberIndex,
  proofVersion,
  verifyUseCaseAssertion,
  withCreatedAt,
} from './_shared.js'

const slug = 'nip05-domain-service-provider-trust'
const domainId = 'nip05:alice@example.com'
const circle = createTrustCircle(defaultMembers.map(member => member.pub))

const contributions = defaultMembers.map((reviewer, index) =>
  contributeIdentifierAssertion(
    circle,
    domainId,
    externalProfileKind,
    { rank: 80 + index * 2 },
    reviewer.priv,
    memberIndex(circle, reviewer.pub),
    { proofVersion },
  ),
)

export const assertion = withCreatedAt(aggregateIdentifierContributions(
  circle,
  domainId,
  externalProfileKind,
  contributions,
  { proofVersion },
))

export const result = verifyUseCaseAssertion(slug, assertion, {
  kind: NIP85_KINDS.IDENTIFIER,
  subject: domainId,
  subjectTag: 'k',
  subjectTagValue: externalProfileKind,
  circleId: circle.circleId,
  minDistinctSigners: 3,
  freshAfter: assertion.created_at - 300,
})

Companion evidence

The proof carries the reviewer circle's assessment. The application still has to prove the underlying service facts it relies on. Model those checks as companion evidence in the signed deployment policy and derive them from collector or resolver output rather than static pass records. The default provider evidence ids are nip05-resolution, https-probe, and dns-owner-check. Use collectNip05DomainCompanionEvidence() when the verifier can fetch the NIP-05 document and HTTPS probe directly, and pass a deployment-specific DNS owner check. Use resolveNip05DomainCompanionEvidence() when those observations came from your own resolver or monitoring service.

const policy = createDeploymentPolicy(NIP05_DOMAIN_SERVICE_PROVIDER_TRUST_PROFILE, {
  companionEvidence: nip05DomainCompanionEvidenceRequirements(subject, { maxAgeSeconds: 300 }),
  expectedSubject: subject,
  expectedSubjectTagValue: '0',
  // accepted circles, metrics, freshness, and signature policy omitted here
})

const companionEvidence = await collectNip05DomainCompanionEvidence({
  checkedAt: now,
  checkDnsOwner,
  expectedPubkey,
  fetch,
  subject,
})

const result = verifyProductionDeployment(assertionFromRelay, bundle, {
  companionEvidence,
  now,
  trustedPublishers,
})

For nip05:<name>@<domain>, nip05-resolution should fetch and check the current NIP-05 document for that name. For broader domain:<host> or service subjects, customise the companion requirements to the exact DNS, HTTPS, LNURLp, NIP-96, or service probe your application depends on. If the application is not acting on a NIP-05 name, remove or mark nip05-resolution optional and require the service-specific probe that proves the fact you will rely on. The lower-level building blocks are fetchNip05DocumentEvidence() and probeHttpsService(). DNS ownership is deliberately a callback because deployments vary: some use DNSSEC, some use account-control checks, and some use an operator-controlled allow-list.

What to verify

  • Strict syntax and a valid proof v2.
  • Kind 30385, with d equal to the exact canonical identifier and k equal to the identity/provider profile namespace.
  • The circle is trusted for identity or provider review, not merely any reputation circle.
  • The assertion is fresh enough for domain or provider risk and has not been superseded by an incident signal.
  • The application has independently resolved the underlying NIP-05, DNS, HTTPS, LNURL, or service check when that check matters. If the policy requires companionEvidence, missing, failed, stale, or wrong-subject evidence must reject the deployment decision.

What this proves

  • A circle rated the exact identifier string.
  • The threshold and aggregate can be verified from the event.
  • Proof v2 binds the contribution to the identifier assertion namespace.

What not to claim

  • Do not claim the proof itself performs NIP-05, DNS, HTTPS, or LNURL resolution.
  • Do not claim a domain-level score proves every name or endpoint on that domain is safe.
  • Do not claim provider trust is permanent. Domains and hosted identities can change owner or behaviour quickly.

Failure handling

  • Reject assertions for non-canonical names, wrong namespaces, unknown circles, stale reviews, or unresolved service checks.
  • Split a broad domain assertion into specific NIP-05, endpoint, or provider assertions when clients need precision.
  • Publish incident, revocation, or downgraded assertions when ownership, hosting, TLS, or service behaviour changes.
  • Fall back to direct resolution and local policy when no trusted circle has reviewed the identifier.

Operational requirements

Risk to handleRequired control
The proof does not prove domain control.Perform DNS, HTTPS, NIP-05, LNURL, or service-specific checks before reviewers contribute.
The proof does not perform NIP-05 resolution.Resolve and cache the NIP-05 result in the application; use nostr-veil only for the circle's assessment of that identifier.
Providers can change behaviour after review.Use expiry, periodic re-review, incident assertions, and revocation once the profile supports it.
DNS, HTTPS, and provider accounts are not made anonymous.Use normal operational security for lookups and account management; nostr-veil hides only which circle members contributed.

Policy choices

  • How are domains canonicalised: lowercase, punycode, trailing slash, scheme?
  • Does rank mean identity confidence, operational reliability, abuse risk, or provider trust?
  • Should a domain and a specific NIP-05 name be separate subjects?
  • What expiry should identity trust carry?

Companion evidence smoke test

The companion-evidence smoke test runs the collector path for this profile and then verifies the derived evidence ids. The checked-in public report is a deterministic fixture dry run so the page stays reproducible; operators can run the same harness in live mode when they want npm, OSV, NIP-05, HTTPS, and relay I/O.

Passed 2026-05-18T14:10:09.000Z
Subject
nip05:jack@primal.net
Mode
deterministic fixture dry run
Evidence
3/3 passing
  • Collector Returned All Evidence
  • Resolver Fetched Current Document
  • nip05-resolution: pass
  • https-probe: pass
  • dns-owner-check: pass

Run the deterministic check with npm run test:companion-evidence. For live I/O, run npm run test:companion-evidence:live.

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
87b41f8d44e7...fedeb349

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