X1ID is live on X1 mainnet
X1ID / Attestation gating

Attestation gating

Gate access to your app, Discord, mint, or airdrop by whether an @handle holds a verified on-chain attestation — any signal, or a specific platform like "verified GitHub." You reuse X1ID's blue-tick oracle as your allowlist instead of running your own verification.

Everything is read from chain: an Attestation is an attestor-signed account, and the staleness rule (it only counts for the current owner — attested_at >= handle.registered_at) is enforced for you. There are two ways in: an off-chain check (your server or app reads chain and decides) and an on-chain CPI (another program verifies the same thing inside its own transaction).

This page targets @x1id/[email protected]. The program is deployed on X1 mainnet at 8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P (https://rpc.mainnet.x1.xyz; testnet works the same with https://rpc.testnet.x1.xyz).

Off-chain gate (SDK)

Your server or app reads the handle's attestations and decides. Two functions cover it — handlePassesGate (fetch + check in one call) and attestationsPassGate (a pure, synchronous check when you already hold the attestations).

import {
  handlePassesGate, attestationsPassGate, GateKind, type Gate,
} from '@x1id/resolve';

// Define the gate:
const anyVerified: Gate = 'any-verified';          // any blue-tick
const githubOnly: Gate  = { kind: GateKind.github }; // one specific platform
const xOrGithub: Gate   = { anyOf: [GateKind.x, GateKind.github] };

// One-call fetch + check. handleAccount / registeredAt / recordsClearedAt come
// from the handle you resolved (name → deriveHandleAccount → decode the Handle):
const { passed, attestations } = await handlePassesGate(
  rpc, programId, handleAccount, registeredAt, recordsClearedAt, githubOnly,
);
if (!passed) throw new Error('Not allowed — verify your GitHub on X1ID first.');

// Already have the attestations? The check is pure + synchronous:
attestationsPassGate(attestations, xOrGithub); // boolean

The Gate shape

A Gate is what a handle must prove to pass:

FormPasses when the handle holds…
'any-verified'at least one non-stale attestation of any kind
{ kind }a non-stale attestation of exactly that kind
{ anyOf: [...] }a non-stale attestation of any listed kind (e.g. X or GitHub)

GateKind names the platform kinds so you gate without magic numbers:

GateKind.dns      // 0
GateKind.social   // 1  (legacy generic)
GateKind.x        // 2
GateKind.discord  // 3
GateKind.github   // 4
GateKind.telegram // 5

handlePassesGate returns both the pass/fail and the attestations, so a caller can also render which signals matched. Stale attestations never count — the staleness rule is baked into HandleAttestation.stale and this module always ignores stale ones.

Resolve the visitor's connected wallet → its primary @handle (reverse resolution) → gate it. Combine with a control-proof challenge (createControlChallenge / verifyControlProof) to first prove the wallet controls the handle, then gate on its attestations — a complete "verified-handle-only" door with no backend of your own.

On-chain gate (CPI): assert_attestation_gate

The SDK path above gates off-chain. To gate on-chain — another program CPI-verifying that the caller controls a verified handle within the same transaction — invoke assert_attestation_gate. It returns Ok(()) when the gate passes and errors (GateNotSatisfied) otherwise, so a CPI that errors fails the caller's transaction: that is the gate.

  • Program: 8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P
  • Discriminator: sha256("global:assert_attestation_gate")[..8] = [101, 166, 24, 233, 23, 22, 25, 204]

Argument — kind: Option<u8> (borsh): 0x00 = any kind passes; 0x01 <kind> requires that specific kind. Codes: 0 dns · 1 social (legacy) · 2 x · 3 discord · 4 github · 5 telegram (the Attestation::KIND_* constants).

Instruction data — disc(8) ++ option(1 or 2):

require GitHub =  [101,166,24,233,23,22,25,204, 1, 4]
any verified   =  [101,166,24,233,23,22,25,204, 0]

Accounts, in order:

#AccountSignerWritableDerivation
0caller✅—the entity proving control (also fee payer); allocates nothing, so not mut
1handle——["handle", name_bytes] — the handle being asserted
2attestation——["attestation", handle_pubkey, [kind]] — seeds re-derive from the attestation's own stored kind

For a tokenized handle (the owner holds the capability pNFT), append the owner's pNFT token account as remaining_accounts[0] — the ATA proof that caller holds the NFT. Omit it for an untokenized handle (the handler reads handle.owner directly). This is the same authority check the record-editing instructions use.

What the handler enforces: (1) caller currently controls the handle (untokenized owner, or pNFT holder); (2) the attestation's kind matches the requested kind (when Some); (3) the attestation is bound to this handle; and (4) the epoch-staleness rule — attestation.attested_at >= handle.registered_at, so an attestation from a previous owner (before a transfer/sale/re-register) does not count.

What it proves — and what it doesn't

Trust in the gate = trust in the attestor, exactly like the blue-tick.

  • Proves: the handle currently holds a non-stale, attestor-signed attestation of the required kind. The attestor is X1ID's verification oracle (the same one behind the profile blue-tick).
  • Does not prove: anything the attestor didn't verify. A verification is an off-chain review (a DNS lookup, an OAuth completion, a human check) that X1ID's admin-rotatable attestor key signed off on — not a trustless proof. Render it as "verified by x1id", never as more.
  • Ownership can change: re-check per session — a handle sold after being verified carries no attestation for its new owner (the staleness rule handles this automatically).

Next

Browse the docs

Attestation gating