Skip to main content

Running an Identity Authority (IA)

Zapf is an open, federated protocol. While zapf.app operates a public Authority, anyone can spin up their own independent Identity Authority (IA).

You might run a custom IA to support a specific community platform, or to offer a high-privacy attestation service free from central oversight.

IA Server Requirements

To operate a compliant IA on the Zapf network, your infrastructure needs:

  1. A Nostr Keypair: A securely generated private key that will act as your Server Identity. You will use this to sign all Attestations.
  2. A Public Nostr Relay: An accessible relay (e.g., wss://relay.yourdomain.com) where you will publish your attestations. This relay must have high uptime, as wallets will query it during the Deep Check verification phase.
note

A Lightning node, custodial ledger, and .well-known/lnurlp/ server are required only if you are also operating as a Zap Settlement Provider (ZSP). Pure IA operation does not require payment infrastructure.

Generating Keys and Evidence

Connection Key

The d tag in Kind 35522 is a deterministic Connection Key derived from the LIDP name and the user's normalized identifier:

ConnectionKey = hex( SHA256( lidp_name + ":" + normalized_lidp_id ) )

Example: SHA256("discord:1254093577051574374") → the lowercase hex string used as the d tag.

Normalization rules differ by LIDP (e.g., lowercase for email, numeric ID for Discord). Always normalize before hashing to ensure uniqueness.

Challenge Token (npv1)

The challenge token cryptographically binds a verification session to a specific user. See the full spec in IA Attestation — The npv1 Challenge Token.

Summary:

  1. Generate a random pre_auth_code (e.g., 6-byte random hex string).
  2. Compute SHA256(hex.Decode(user_pubkey) || []byte(pre_auth_code)).
  3. Prepend the 2-byte TLV header 0x00 0x20.
  4. Bech32-encode the 34-byte result with the npv1 human-readable part.

The user must publish this token publicly (in a Discord message, tweet, DNS TXT record, etc.) at a URL you will record as evidence_url.

Evidence Payload

After the user publishes the challenge, construct the evidence JSON:

{
"version": 1,
"lidp": "discord",
"auth_type": "public_post",
"user_id": "1254093577051574374",
"username": "joyosar",
"verified_at": 1779219590,
"evidence_url": "https://discord.com/channels/.../1506380827804831834",
"challenge": "npv11qqsykd7ufyvfjl9qasdgtrz02jsv97l9atdnqc0vz8wsytxqxn9v6pqzvtph4",
"pre_auth_code": "feb7dee63337"
}

pre_auth_code must always be included. Cross-IA re-attestation requires it to verify the challenge is genuinely bound to this user and session. See Cross-IA Challenge Binding.

The Attestation Lifecycle

As an IA operator, you are responsible for the entire lifecycle of the identity connections you certify.

Signing and Confirming

When a user authenticates with a LIDP via your service, you strictly normalize their identifier, generate the cryptographic ConnectionKey, and publish an Attestation to your relay. The session moves to confirmed status.

At this point, the Kind 35522 is on the relay but the LIDP→Nostr routing link is not yet active. The identity record is not written to the store until the user completes the activation step.

User Activation (Required)

After the Kind 35522 is published, your IA returns the raw attestation event to the user's client. The client extracts the event ID and your relay URL, builds a lightweight e tag reference, and publishes a Kind 35521 event — without embedding the full attestation JSON. The client then sends an activation signal to your IA to confirm that Kind 35521 is live.

Only after receiving this signal does your IA write the Identity record to its store — making LIDP→Nostr routing live. The session moves to active status. If the user closes the dialog or declines without completing the activation step, no routing link is created.

note

The activation signal mechanism is implementation-defined. Your IA must expose some interface for clients to trigger this step; the specific protocol (HTTP endpoint, Nostr event, etc.) and its path are entirely up to your implementation.

Dealing with Expiration

Set a NIP-40 expiration timestamp ↗ on every Attestation. The default is 90 days (configurable via IA_ATTESTATION_EXPIRY_DAYS). When an attestation expires, clients prompt the user to re-verify, ensuring identity ownership is periodically re-confirmed.

Active Revocation

If a user disconnects their identity from your dashboard, or if you detect abusive behavior, your IA must immediately publish a Kind 5 deletion event targeting the original Attestation. When wallets perform a Deep Check on your relay, they will see the deletion and reject out-of-date attestations.

If the session was already active (user had signed and published Kind 35521), the IA must also delete the Identity routing record from its store to stop all zap routing to that link immediately.

Summary of session states

StatusKind 35522 on relayIdentity in store (routing live)
confirmedYesNo
activeYesYes
revokedDeleted (Kind 5)No

Establishing Trust

Because Zapf is permissionless, simply running an IA does not mean the network will use it.

Wallets and client applications (like the zapf.app web client) maintain a list of Trusted IA Pubkeys. To get your IA recognized broadly across the ecosystem, you must build a reputation for securely verifying identities, protecting user data, and maintaining stable infrastructure.