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:
- A Nostr Keypair: A securely generated private key that will act as your Server Identity. You will use this to sign all Attestations.
- 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.
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:
- Generate a random
pre_auth_code(e.g., 6-byte random hex string). - Compute
SHA256(hex.Decode(user_pubkey) || []byte(pre_auth_code)). - Prepend the 2-byte TLV header
0x00 0x20. - Bech32-encode the 34-byte result with the
npv1human-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.
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
| Status | Kind 35522 on relay | Identity in store (routing live) |
|---|---|---|
confirmed | Yes | No |
active | Yes | Yes |
revoked | Deleted (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.