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); // booleanThe Gate shape
A Gate is what a handle must prove to pass:
| Form | Passes 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 // 5handlePassesGate 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.
The recommended pattern
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:
| # | Account | Signer | Writable | Derivation |
|---|---|---|---|---|
| 0 | caller | ✅ | — | the entity proving control (also fee payer); allocates nothing, so not mut |
| 1 | handle | — | — | ["handle", name_bytes] — the handle being asserted |
| 2 | attestation | — | — | ["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).
