X1ID is live on X1 mainnet
X1ID / Verified identity

Verified identity

Beyond the per-chain verification a resolve() returns, a handle can carry per-platform verifications — the blue-tick signal that a handle proved control of an X, Discord, GitHub, or Telegram account, or a domain. This page is how you read that signal.

This page targets @x1id/[email protected]. The registry is on X1 mainnet at 8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P (https://rpc.mainnet.x1.xyz).

Two ways to read it

A verification can reach you two ways, and the helpers tolerate either:

  1. From a Resolved's verifications[] — when the producing resolver supplies it (the tools/api REST /resolve response carries this field). The SDK's own on-chain resolve() does not populate it.
  2. Straight from chain — read the Attestation accounts yourself with fetchAttestations. This applies the staleness rule structurally and needs no REST layer.

From a resolve result

If your Resolved carries verifications[], three absence-tolerant helpers read it:

import { isVerified, verifiedPlatforms, isVerifiedOn } from '@x1id/resolve';

// `resolved` is a Resolved carrying the resolver's verifications[], e.g.
//   verifications: [
//     { platform: 'x',      value: '@michelle',    verified: true, attested_at: 1730000000 },
//     { platform: 'github', value: 'michelle',     verified: true, attested_at: 1730000100 },
//     { platform: 'domain', value: 'michelle.com', verified: true, attested_at: 1730000200 },
//   ]

isVerified(resolved);              // true  — show the blue tick (any live signal)
verifiedPlatforms(resolved);       // ['x', 'github', 'domain']
isVerifiedOn(resolved, 'github');  // true
isVerifiedOn(resolved, 'telegram'); // false

All three are absence-tolerant: an older resolver, the SDK's own on-chain resolve(), or a handle with no live verifications all yield false / [], never a throw. Never index into verifications blindly — go through these helpers.

Rendering a badge. Light the tick when isVerified(resolved) is true; on hover/focus, list verifiedPlatforms(resolved) and pull each platform's account from the paired value (@michelle for x, michelle.com for domain). Platform slugs are x / discord / github / telegram, plus legacy domain (DNS) and social (pre-per-platform, no attribution).

Straight from chain

To compute the same signal without a REST layer, read the Attestation accounts directly. Every function demands the handle's registeredAt and recordsClearedAt so the staleness rule is applied structurally — there is no way to fetch an attestation without them.

import {
  fetchAttestations, liveAttestations, isHandleVerified,
  platformSlug, textRecordKeyFor,
} from '@x1id/resolve';

// handleAccount / registeredAt / recordsClearedAt come from the Handle you
// resolved (name → deriveHandleAccount → decode the Handle account).
const atts = await fetchAttestations(rpc, programId, handleAccount, registeredAt, recordsClearedAt);

isHandleVerified(atts); // the blue-tick condition, from chain

for (const a of liveAttestations(atts)) {
  platformSlug(a.kind);     // 'x' | 'discord' | 'github' | 'telegram' | null (legacy)
  textRecordKeyFor(a.kind); // 'com.x' | ... | 'website' | null — where the paired value lives
}
  • fetchAttestations returns every attestation of a handle, stale ones flagged, so an owner surface can show what a previous registration left behind. Anything that renders a badge should take isHandleVerified / liveAttestations.
  • isHandleVerified implements the rule below: true when at least one non-stale attestation of any kind exists.
  • platformSlug / textRecordKeyFor mirror the on-chain Attestation::platform_slug and the shared contract table exactly, so joining a live attestation to its paired text record uses the same mapping the program, resolver and app badge use.

Attestation kinds

KindConstantSlugPaired text record
0ATTESTATION_KIND_DNSdns (legacy)website
1ATTESTATION_KIND_SOCIALsocial (legacy)—
2ATTESTATION_KIND_Xxcom.x
3ATTESTATION_KIND_DISCORDdiscordcom.discord
4ATTESTATION_KIND_GITHUBgithubcom.github
5ATTESTATION_KIND_TELEGRAMtelegramorg.telegram
6KIND_VALIDATORvalidator— (vote account in evidence_hash)

The two legacy kinds (dns, social) are kept valid forever so existing accounts are never orphaned; platformSlug returns null for them because neither names a single modern platform. A HandleAttestation also exposes account, handle, kind, kindName, evidenceHash, attestedAt, attestor, and stale.

Kind 6 is the validator shield, not a blue-tick platform. Unlike kinds 0–5, it is minted trustlessly on-chain (no attestor) and surfaces as a distinct gold shield, so render it separately rather than as a social-verification row. Its evidence_hash holds the validator's vote account. See Verified validators for both instructions, the challenge format, and the Verify-your-validator flow.

Verified = one live signal of any kind

A handle is verified when at least one non-stale attestation of any kind exists — a single strong signal is enough (requiring two would reject Nike proving control of nike.com). kind records which signal proved it.

The staleness rule

An attestation only vouches for the current owner when it is live. A stale attestation belongs to a previous, unrelated owner of the same name and must never be rendered as verifying the current one.

A Handle's address is ["handle", name] — a pure function of the name — so release + re-register lands the new registration at the same pubkey, and the previous owner's attestation is physically attached to the new owner's name with no action by anyone. Attestations do not expire on a timer; they are invalidated by ownership epoch. An attestation is stale (and so excluded by liveAttestations / isHandleVerified) when either:

  • attested_at < handle.registered_at — predates the current registration; or
  • attested_at < handle.records_cleared_at — the owner ran clear_records, bulk-invalidating records and attestations without a transfer.

One documented blind spot, shared with Handle.owner and Primary: a bearer-NFT marketplace trade bumps no epoch, so an attestation keeps reading live until the attestor re-reviews or revokes. Re-check verification per session for anything high-stakes.

What a verification proves — honestly

A verification is an off-chain review (a DNS lookup, an OAuth completion, a human check) that X1ID's attestor key signed off on at attested_at. It is not a trustless proof: that key is admin-rotatable, so the trust anchor is "whoever the registry admin currently designates." Render it as "verified by x1id", never as more. (The trustless upgrade for the domain signal is DNSSEC/zkTLS, on the roadmap.)

Next

Browse the docs

Verified identity