# Attestation gating
URL: /docs/x1id/gating

> Gate your app, Discord, mint, or airdrop by whether an @handle holds a verified on-chain attestation — reuse X1ID's blue-tick oracle as your allowlist instead of running your own verification.

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

<LlmActions />

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

<Callout type="info">
  This page targets **`@x1id/resolve@0.21.0`**. The program is deployed on **X1 mainnet** at `8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P` (`https://rpc.mainnet.x1.xyz`; testnet works the same with `https://rpc.testnet.x1.xyz`).
</Callout>

## 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).

```ts
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:

| 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:

```ts
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.

### The recommended pattern

Resolve the visitor's connected wallet → its primary `@handle` ([reverse resolution](/docs/x1id/sdk#reverse)) → 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.**

<Callout type="info">
  * **Program:** `8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P`
  * **Discriminator:** `sha256("global:assert_attestation_gate")[..8]` = `[101, 166, 24, 233, 23, 22, 25, 204]`
</Callout>

**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

<Callout type="warn">
  **Trust in the gate = trust in the attestor**, exactly like the blue-tick.
</Callout>

* **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

<Cards>
  <Card href="/docs/x1id/verification" icon="BadgeCheck" title="Reading verified identity" description="The blue-tick helpers: isVerified, isVerifiedOn, verifiedPlatforms, fetchAttestations." />

  <Card href="/docs/x1id/sdk" icon="Code" title="SDK reference" description="Resolve a wallet to its primary @handle before you gate it." />

  <Card href="/docs/x1id/contracts" icon="FileCode2" title="Contracts" description="The registry program, its RPC, and how to verify it yourself." />
</Cards>
