Chapter 39 - Identity
39
Nostr Implementation Possibility - Hex № 39

Linking Profiles to Other Platforms

One event lists your accounts on other platforms, each with a public proof that anyone can check.

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.

Hint!

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.

Wire format - kind 10011 identity claims
{
  "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:

i tag parameters
#ParameterRule
1platform:identityThe platform name (for example github) and the identity on that platform (for example semisol), joined with :.
2proofA string or object that points to the proof of owning this identity.

Three normative rules apply to the tag:

  • Clients should process i tags 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.
"i" "github:semisol" "9721ce4e…a5ab" tag name platform : identity proof (here: a Gist ID) no ":" allowed in the platform name
Fig. 39-1 — i tag anatomy. Two parameters: who you are over there, and where the proof lives.
Tech Note

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.

Claim types
PlatformIdentityProofProof location
githuba GitHub usernamea Gist IDhttps://gist.github.com/<identity>/<proof>
twittera Twitter usernamea Tweet IDhttps://twitter.com/<identity>/status/<proof>
mastodon<instance>/@<username>a post IDhttps://<identity>/<proof>
telegrama 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>.

twitter

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>".

Tech Note

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.

Warning!

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.

Client Relay Platform REQ {"kinds":[10011],…} EVENT with i tags GET gist.github.com/semisol/9721ce4e… "Verifying that I control … npub1…" npub in proof == event pubkey? → verified Repeat for every i tag. A failed fetch or mismatch = unverified claim.
Fig. 39-2 — Verification flow. The relay supplies the claim. The other platform supplies the proof. The client compares.

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:

Alice's identity 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:

Verification session - Bob's client
-- 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 i tags out of the kind-0 metadata event into the new replaceable kind 10011.
  • 20242024Clarified that i tags belong in the tags array; JSON and link formatting fixes.
  • 20242024-02Added a pgp claim 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

Authorpseudozach
First commit2023-03-09 — “NIP-39 external identities in metadata”
Statusdraft - optional — clients may ignore identity claims entirely
Source39.md on GitHub - full change history

6 See Also

  • NIP-01 — the event format, the tags array, and the replaceable storage class of kind 10011.
  • NIP-19 — the npub encoding 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.