1 Overview
A Nostr public key is just hex. It does not tell other users who you are. This NIP lets a user declare: this GitHub account, this Twitter account, this Mastodon account, and this Telegram account are all mine.
The declaration alone proves nothing. Anyone can type someone else's username. So each claim carries a proof: a post the user made on the other platform that names their Nostr public key. A client fetches the proof and compares the keys. If they match, the link is real in both directions.
Claims live in a dedicated event of kind 10011, as i tags. The kind is replaceable under the NIP-01 storage rules, so a relay keeps only the latest identity list per pubkey.
NIP-39 links you to other platforms. To link a Nostr key to a domain name you control, use NIP-05 instead. The two work well together.
2 The Identity Event and the i Tag
Publish one event of kind 10011. Put one i tag in its tags array for each claimed identity.
{
"kind": 10011,
"tags": [
["i", "github:semisol", "9721ce4ee4fceb91c9711ca2a6c9a5ab"],
["i", "twitter:semisol_public", "1619358434134196225"],
["i", "mastodon:bitcoinhackers.org/@semisol", "109775066355589974"],
["i", "telegram:1087295469", "nostrdirectory/770"]
],
// other fields...
}
An i tag must have two parameters:
| # | Parameter | Rule |
|---|---|---|
| 1 | platform:identity | The platform name (for example github) and the identity on that platform (for example semisol), joined with :. |
| 2 | proof | A string or object that points to the proof of owning this identity. |
Three normative rules apply to the tag:
- Clients should process
itags with more than 2 values. Extra values allow future extension. - A platform name should use only
a-z,0-9, and the characters._-/. It must not include:. - Normalize identity names when possible. Replace uppercase letters with lowercase letters. If an entity has several aliases, use the primary one.
The example object in the source spec is not valid JSON: a comma is missing between the mastodon and telegram tag lines. The example above adds it. Also, the abstract mentions claims over "keypairs", but the spec defines no keypair claim type. A pgp type existed for three days in 2024-02 and was removed "temporarily"; it never returned.
3 Claim Types
The spec defines four platforms. For each one it fixes three things: what the identity string is, what the proof points to, and the exact text the proof post must contain. <npub> below is the user's public key in the bech32 npub encoding from NIP-19.
| Platform | Identity | Proof | Proof location |
|---|---|---|---|
| github | a GitHub username | a Gist ID | https://gist.github.com/<identity>/<proof> |
| a Twitter username | a Tweet ID | https://twitter.com/<identity>/status/<proof> | |
| mastodon | <instance>/@<username> | a post ID | https://<identity>/<proof> |
| telegram | a Telegram user ID | <ref>/<id> | https://t.me/<proof> |
github
The proof is a Gist that <identity> created. The Gist has a single file with the text: Verifying that I control the following Nostr public key: <npub encoded public key>.
The proof is a Tweet that <identity> posted. The Tweet has the text: Verifying my account on nostr My Public Key: "<npub encoded public key>".
mastodon
The proof is a post that <username>@<instance> published. The post has the text: Verifying that I control the following Nostr public key: "<npub encoded public key>".
telegram
The proof is a message in the public channel or group named <ref>, with message ID <id>. User ID <identity> sent the message. The message has the text: Verifying that I control the following Nostr public key: "<npub encoded public key>".
The required proof text is not uniform. The github text has no quotes around the npub; the other three quote it. The twitter text uses a different sentence and no final period, exactly as written in the spec. A verifier must match the exact per-platform string, not one shared template.
Always verify. An i tag with a missing, deleted, or mismatched proof is only a claim. Show unverified claims differently, or not at all. Never treat the tag alone as evidence.
4 Worked Example
Alice controls the GitHub account alice-dev and the Mastodon account @alice@mastodon.social. She posts a proof on each platform, then publishes one kind 10011 event:
{
"id": "7f3c1a9d20…e4b8",
"pubkey": "a1b2c3d4e5…9f00",
"created_at": 1767225600,
"kind": 10011,
"tags": [
["i", "github:alice-dev", "c3d9e2f8a41b7c60d5a2e91f0b8c4d73"],
["i", "mastodon:mastodon.social/@alice", "113954218847001122"]
],
"content": "",
"sig": "88d1a0f2c7…31ab"
}
Bob's client receives this event and verifies each claim:
-- fetch the claims from a relay -- → ["REQ", "ids", {"kinds":[10011], "authors":["a1b2c3d4e5…9f00"]}] ← ["EVENT", "ids", {"kind":10011, "tags":[["i","github:alice-dev","c3d9e2f8…"], …]}] ← ["EOSE", "ids"] -- claim 1: github. Build the proof URL and fetch it -- GET https://gist.github.com/alice-dev/c3d9e2f8a41b7c60d5a2e91f0b8c4d73 file body: "Verifying that I control the following Nostr public key: npub15dj…8ftq" npub15dj…8ftq decodes to a1b2c3d4e5…9f00 → MATCH. Verified. -- claim 2: mastodon -- GET https://mastodon.social/@alice/113954218847001122 post text: "Verifying that I control the following Nostr public key: \"npub15dj…8ftq\"" → MATCH. Verified. Bob's client shows both badges on Alice's profile.
5 Version History
NIP-39 started as extra tags inside the kind-0 metadata event. In 2026 the claims moved into their own replaceable kind. Notable changes, newest first:
- 20262026-06Renamed to "Linking Profiles to Other Platforms" for clarity.
- 20262026-02Moved
itags out of the kind-0 metadata event into the new replaceable kind10011. - 20242024Clarified that
itags belong in thetagsarray; JSON and link formatting fixes. - 20242024-02Added a
pgpclaim type, then removed it "temporarily" three days later. - 20232023-11Author lists removed from all NIPs; JSON reformatting.
- 20232023-03Readability pass by Semisol days after the merge.
- Origin2023-03-09pseudozach merged the first version: "NIP-39 external identities in metadata".
Original Proposal - Credits
| Author | pseudozach |
| First commit | 2023-03-09 — “NIP-39 external identities in metadata” |
| Status | draft - optional — clients may ignore identity claims entirely |
| Source | 39.md on GitHub - full change history |
6 See Also
- NIP-01 — the event format, the
tagsarray, and the replaceable storage class of kind10011. - NIP-19 — the
npubencoding that every proof text must contain. - NIP-05 — maps a Nostr key to a DNS-based identifier; the domain-name counterpart to this NIP.
- NIP-24 — extra fields for the kind-0 metadata event, where these claims used to live.