Adding a Legacy Identity Provider
As a permissionless protocol, Zapf lets anyone integrate a new Legacy Identity Provider (LIDP).
Whether you are adding a new social platform, messaging service, or custom authentication system, the process follows a standardized pattern.
Understanding the Role
When you add a LIDP, you are teaching a Zap Settlement Provider (ZSP) how to verify ownership of a platform account and how to construct the standardized Nostr events (Identity Connection and IA Attestation) that prove it.
The Integration Pattern
Choose the verification method that fits the platform, then implement these shared steps:
Step 1: Choose a Verification Method
Bot Challenge (Discord, Telegram, and similar messaging platforms):
- The ZSP runs a bot on the platform.
- The user runs
/nostr-verify <pre_auth_code>(Discord) or/nostr_verify <pre_auth_code>(Telegram) in a community channel; the bot responds with annpv1challenge token. - The user posts the challenge publicly in the same channel and submits the message link (
auth_type: "public_post"— same model as other challenge-token providers). - Best for platforms where the ZSP can operate a verified bot identity.
Challenge Token (X/Twitter, GitHub, Instagram, Facebook, Domain):
- The ZSP issues a unique token tied to the user's pending session.
- The user publishes that token on their account (tweet, gist, bio, DNS TXT record, etc.).
- The ZSP fetches and verifies the published content via the platform's public API or HTTP.
- Best for platforms with public content APIs.
OTP (Email, Phone):
- The ZSP sends a one-time code to the user's contact address or device.
- The user submits the code to complete verification.
- Best for private channels that don't support public proof publishing.
Step 2: Identifier Normalization
Before signing any attestations, the ZSP must normalize the identifier to prevent spoofing:
- Email addresses: lowercase and strip aliases (e.g.
user+spam@gmail.com→user@gmail.com). - Phone numbers: E.164 format (e.g.
+1234567890). - Platform IDs: use the stable numeric user ID (not the username) as the raw identifier for ConnectionKey generation, since usernames can change on many platforms. Store the username separately as the display handle.
- Domain: normalize to lowercase, strip trailing dots.
Step 3: ConnectionKey Generation
Generate the deterministic ConnectionKey for the Nostr events:
rawString := fmt.Sprintf("%s:%s", lidpName, normalizedIdentifier)
connectionKey := sha256(rawString)
The normalizedIdentifier is the stable numeric ID (e.g. Discord snowflake, Telegram user ID) — not the username. This ensures the ConnectionKey stays the same even if the user changes their username.
Store the username separately in the Identity.Username field so it can be embedded in zap receipt tags as a human-readable handle.
Step 4: Constructing the Evidence Payload
Build an evidence object recording the verification proof. All modes produce the same v1 format — always cleartext JSON, never encrypted:
{
"version": 1,
"lidp": "discord",
"auth_type": "public_post",
"user_id": "1254093577051574374",
"username": "loki_nakamo",
"verified_at": 1720000000,
"evidence_url": "https://discord.com/channels/.../1506380827804831834",
"challenge": "npv11qqsykd7ufyvfjl9qasdgtrz02jsv97l9atdnqc0vz8wsytxqxn9v6pqzvtph4",
"pre_auth_code": "feb7dee63337"
}
For OTP-based flows (e.g. email), evidence_url, challenge, and pre_auth_code will be absent since there is no public post. Cross-IA re-attestation is not possible for those connections.
Serialize this to JSON and attach it as the evidence tag value on the Kind 35522 Attestation. The evidence is always cleartext — do not encrypt it. See Evidence JSON Schema for the full field reference.
Step 5: Signing and Publishing
Construct the Attestation:
- Set the connection key.
- Set the user's Nostr public key (if already registered).
- Set the provider string (e.g.
"discord"). - Set an appropriate
expiration.
Sign the event with the IA's private key and publish it to the designated Nostr relays.