NIP-39 X identity linking
Attention uses NIP-39
kind 10011 events to publish verified links between a Nostr public key and an
X account.
Event shape#
Each link is a replaceable kind 10011 event signed by the claiming Nostr key.
Attention requires exactly two X i tags:
twitter:<handle>— the current public username, normalized to lowercase.twitter_id:<numeric-id>— the stable numeric X user ID.
Bio-backed discovery still uses the public profile Bio as an X-side hint.
Kind 10011 itself is a signed claim and does not require that Bio: two-element
tags are enough because the event signature proves key control:
["i", "twitter:<handle>"]
["i", "twitter_id:<numeric-id>"]
Legacy post-backed claims may include the same X proof-post ID as element 3.
Clients that support stable references should prefer twitter_id and treat
the handle tag as informational.
For post-backed claims, Attention appends a fourth, structured subject hint
that repeats the evidence post as post:id:<same-id>. The standard raw
numeric proof-post ID remains in element 3 for compatibility. A legacy
three-element tag is still valid; when the fourth element is present it MUST
match element 3.
{
"kind": 10011,
"tags": [
["i", "twitter:nasa", "2080659774136291424", "post:id:2080659774136291424"],
["i", "twitter_id:11348282", "2080659774136291424", "post:id:2080659774136291424"]
],
"content": ""
}
The fourth element is an Attention extension, not a replacement for the NIP-39
evidence field. Implementations interoperating with strict clients SHOULD
accept legacy three-element tags and SHOULD ignore the optional fourth hint
when they do not support structured subject hints. Attention validators
require the fourth value, when present, to be the canonical
post:id:<same-id> form.
When publishing an update, Attention queries the author's current kind 10011
replacement, removes prior twitter and twitter_id tags, inserts the new
pair, and preserves unrelated provider tags and existing content.
Revocation#
To revoke an X claim, publish a newer replaceable kind 10011 with all
twitter / twitter_id tags removed, preserving unrelated provider tags and
content. A signed kind 10011 without Twitter tags is valid as a slot winner
but is not an identity claim — claim validation still requires both tags.
Revocation is latest-wins only. Relays retain historical claim events; readers
that ignore replaceable-event semantics may still surface an older claim.
Attention treats the current addressable slot winner as authoritative and
clears local nip39* columns when the winner has no Twitter claim.
Popup Unlink (User settings) offers: publish clear 10011 → suggest stripping
the npub from the X bio → clear local xIdentities sides → unbind
boundTwitterId.
X-side evidence#
Primary linking UX is Update bio (popup): open the X profile and its Edit
profile dialog, then prepare a suggested description from the saved
profile text with npub1… (nostr) (drop (nostr) when the 160-character X
bio limit is tight). The user copies and saves it. Attention never writes
the X bio. The wizard always suggests the bound npub (confirmReplace: true).
Completion is the profile observer writing xNpub, then binding completeness
(bioOk / bioMismatch). Overlay Check again is a user kick that ingests the
saved UserDescription through that same path — not a poll. A foreign npub is
step 3 status, not a confirm gate. Unlink still uses confirmReplace /
removeNpub on buildSuggestedXBio and opens https://x.com/settings/profile.
The current popup publishes kind 10011 after the active account's numeric
X ID is known and the operator Nostr key is bound to that X. Bio is an
independent public hint (anyone can put any npub in a profile). Step 3 signs
and queues the Nostr event only; it never needs, opens, or creates an X post.
Attention still accepts existing canonical NIP-39-style proof posts for
twitter as a secondary compatibility and discovery path:
- Post from the linked X account.
- Text includes:
Linking my account to Nostr: <npub>. - The raw post ID is the third parameter on each
itag. Attention addspost:id:<same-id>as the fourth parameter when the proof post is available.
Discovery and verification also accept looser ecosystem wording (for
example “Verifying my account on nostr… My Public Key: …”) when the post
embeds exactly one valid npub and an intent cue (nostr plus
link/verify/public-key language). Bare npub spam and multi-npub posts are
rejected. Composer output stays the Linking template.
Passive allowlisted GraphQL timeline/detail JSON may emit Bio npub candidates
(REPORT_X_BIO_CANDIDATES) and post-proof candidates
(REPORT_X_PROOF_CANDIDATES). Bio is the primary X source (single npub1… in
legacy.description); post proofs remain secondary and are taken from that
already-loaded post. Attention does not search X or request oEmbed for them. A post
counts only when it is already visible in an allowlisted timeline or detail
response.
xIdentities stores per-source dates: xDate (bio-carrying post time),
postDate (proof-post created_at), nip39Date (signed 10011 created_at),
eventDate (selected 32009 created_at). Newer source dates win within a
source. The hop is Bio, else a linking post, else a WoT-gated 32009 hint.
Kind 10011 does not open a hop. It confirms the bio when the same npub's
claim names this X id (see .cursor/rules/x-identity.mdc).
The backend implements PREPARE_X_BIO_EDIT for the bio linking UX. Legacy
proof-text generation remains for secondary compatibility paths, not the
current Bindings setup.
Kind 10011 is checked from its signature and matching twitter /
twitter_id tags. Attention does not fetch the X profile
page to confirm that claim. The event is stored per pubkey. nip39* is
written only when that pubkey is already the row's bio npub and the claim
names that X id. proofSource records which source currently supplies the
hop npub (bio, post, or 32009). Confirmation is that match, not a separate
hop.
Identity resolution and trust subjects#
Durable identity storage is xIdentities, keyed by twitterId. Backend
lookups always use that numeric ID. The row’s handle is the latest mutable
username (for X.com URLs), not a primary key.
When a handle must be resolved to a numeric ID (e.g. before an observation exists), Attention tries, in order:
- a sanitized page-world observation pairing
rest_idand username; - a verified kind
10011claim (bothtwitterandtwitter_idtags).
Conflicting numeric IDs remain unresolved instead of being silently selected.
Trust is separate from identity linking. Kind 32009 account statements use
user:id:<numeric-id> with optional k = user:id and s=x.com for new X
statements, and product person trust uses c=identity. Older empty-context
user statements remain valid. Attention does not publish durable profile trust keyed only by
handle. Post statements use post:id:<post-id> with optional k = post:id
and s=x.com, with no c tag. A 32009 i subject MAY
carry a bare npub1… hint for the subject's linked pubkey; that hint is
advisory fallback metadata and does not replace Bio/post evidence or the
signed, independently verified kind 10011 claim. See docs/NIP-32009.md.
NIP-39 wire names remain twitter / twitter_id.
Background API and Phase D gap#
The versioned background API includes:
{
type: 'GENERATE_X_PROOF',
version: 1,
handle: 'nasa',
twitterId: '11348282'
}
{
type: 'VERIFY_X_PROOF',
version: 1,
event: kind10011Event
}
{
type: 'PUBLISH_X_IDENTITY',
version: 1,
handle: 'nasa',
twitterId: '11348282',
proofTweetId: '2080659774136291424'
}
PUBLISH_X_IDENTITY verifies the proof before signing, stores the event and
outbox state in IndexedDB, and then attempts per-relay delivery.
The current Bindings UI verifies the active numeric account, then explicitly
publishes a proofless kind 10011. Bio is a separate optional hint and is not
required. It performs no X account action.