# Developer platform
URL: /docs/x1id/platform

> The keyed /v1 API at api.x1id.io — Connect sign-in, Gate, Agents, the Universal Resolver, and events, all behind one X-API-Key. How to get a key, the rate-limit tiers, and the pay-per-call x402 alternative.

# Developer platform

<Pill tone="mainnet">Mainnet</Pill>

<LlmActions />

The X1ID **developer platform** is a hosted JSON API under `https://api.x1id.io/v1`. It packages the things you'd otherwise build yourself on top of the [`@x1id/resolve`](/docs/x1id/sdk) SDK — sign-in, access gating, agent discovery, cross-namespace resolution — behind one key.

<Callout type="info">
  **Two surfaces, one host.** The [Resolve API](/docs/x1id/api) (`api.x1id.io/resolve/…`) is **public and keyless** — resolution is a read anyone can do. Everything on this page lives under the **`/v1`** prefix and is **keyed**: a key identifies which app is calling, so the platform can rate-limit, meter, and (for Connect) bind a session to your app.
</Callout>

## Base URL and authentication

```
https://api.x1id.io/v1
```

Send your key in the **`X-API-Key`** header on every `/v1` request:

```bash title="terminal"
curl https://api.x1id.io/v1/resolve/x1 \
  -H "X-API-Key: $X1ID_API_KEY"
```

<Callout type="warn">
  **API keys are server-only.** Never put an X1ID API key in browser code, a mobile bundle, or anything a user can download. Call `/v1` from your own backend and attach `X-API-Key` there. For browser sign-in, proxy the `/v1/auth/*` calls through your backend, as shown in [Sign in with X1ID](/docs/x1id/connect#keep-your-key-on-the-server).
</Callout>

Every non-2xx response is a uniform `{ code, message }` body, so you can branch on `code` instead of parsing prose:

```json
{ "code": "API_KEY_REQUIRED", "message": "provide an X-API-Key header. Request a key at https://developers.x1id.io (or contact support@x1id.io)." }
```

`API_KEY_REQUIRED` is what a **keyless** call to a metered endpoint gets **where x402 is not enabled**. Where [x402](#pay-per-call-with-x402) *is* enabled, that same keyless call instead returns **HTTP `402 Payment Required`** (describing what to pay), not `API_KEY_REQUIRED` — the pay-per-call path, no key needed.

<Callout type="info">
  **The one exception to `{ code, message }`:** the `402` challenge body is an x402 **payment-requirements** object — `{ x402Version, accepts: [ … ] }`, where each `accepts[]` entry carries the `scheme`, the CAIP-2 `network` (on X1 mainnet, `solana:4SvBP3omtvcCVWdxq1zBY5cDp4wndjsT`), `asset`, `payTo`, `amount` (smallest units, as a decimal string), and `maxTimeoutSeconds`; the X1-native entry also carries `extra` (with `X-Payer`: `feePayer`, the request-bound `reference` and `requestHash`). A USDC-via-CDP entry, where offered, uses the x402 v1 field name `maxAmountRequired` instead of `amount`. It is the x402 wire format, not an error envelope — detect a `402` by its status, then read `accepts[]`, not a `code`.
</Callout>

## Get a key

Keys are self-serve from the **Developer Portal** at [developers.x1id.io](https://developers.x1id.io). The portal signs you in with [X1ID Connect](/docs/x1id/connect) — your wallet is the identity, no password — and then lets you create, list, and revoke keys for your account, and watch your usage.

<Callout type="warn">
  **A key's secret is shown once, at creation.** Store it somewhere safe the moment it's revealed; the portal keeps only a hash and can't show it again. Lost a secret? Revoke the key and create a new one.
</Callout>

The portal is backed by four endpoints, all authenticated by your Connect **bearer session** (not an `X-API-Key`):

| Endpoint              | Does                                                   |
| --------------------- | ------------------------------------------------------ |
| `POST /v1/keys`       | Create a key — the one-time secret is in the response. |
| `GET /v1/keys`        | List your keys, each with its tier and recent usage.   |
| `DELETE /v1/keys/:id` | Revoke one of your keys.                               |
| `GET /v1/usage`       | Your request counts, tier, and current limits.         |

## Tiers and rate limits

Every key carries a tier. Both windows are **per key**, shared across every `/v1` endpoint the key calls.

| Tier        | Burst       | Daily         | For                                                                             |
| ----------- | ----------- | ------------- | ------------------------------------------------------------------------------- |
| **free**    | 300 / min   | 15,000 / day  | Evaluating and building. Self-serve.                                            |
| **partner** | 1,000 / min | 100,000 / day | Production integrations. Issued after an agreement — contact `support@x1id.io`. |

Each response carries your live budget in headers:

```
X-RateLimit-Limit            300
X-RateLimit-Remaining        299
X-RateLimit-Daily-Limit      15000
X-RateLimit-Daily-Remaining  14999
```

Over the limit returns `429 { "code": "RATE_LIMITED", … }`. A keyed request that's over its window is **not** silently charged per call and does **not** fall through to x402: it gets the `429` until the window resets. x402 is a separate path for requests that send **no** key (see below), not an overflow for a key that hit its limit. To get more headroom, raise your tier.

## Pay-per-call with x402

You don't strictly need a key. Where it's enabled, the metered read endpoints also accept an **[x402](/docs/x1id/get-paid#agents-can-pay-too-x402)** payment: call with no key, get a `402 Payment Required` describing what to pay, pay on-chain, sign the challenge's `redeemMessage` with the wallet that paid, and retry with a `PAYMENT-SIGNATURE` header carrying both signatures (the v1 name `X-PAYMENT` is still accepted). On success the response carries a `PAYMENT-RESPONSE` header (and the v1 `X-PAYMENT-RESPONSE`) confirming settlement. Send `X-Payer` with your wallet address on both requests: it is **required** for the X1-native rail (see [below](#bind-your-payer-with-x-payer-on-the-first-request)).

The order a request is tried is fixed: a valid `X-API-Key` is served (or rate-limited with `429`); a key that's present but invalid gets `401 INVALID_KEY`; only a request with **no** key reaches x402. The [Connect](/docs/x1id/connect) `/v1/auth/*` endpoints and Gate policy management are keyed-only and not x402-metered.

This is the no-signup, no-contract path — ideal for autonomous agents. The X1-native rail is self-sponsored XNT on X1 (the buyer pays their own gas); a USDC-via-CDP rail can also be offered. Default prices are a fraction of a cent per call (resolve/reverse cheaper than verify/gate) and are tunable per deployment.

<Callout type="warn">
  **Breaking (2026-10-11): redemption now needs the payer's signature.** api.x1id.io refuses a payment header without `payload.payerSig` (`REDEEM_SIGNATURE_REQUIRED`), so a client that only sends `payload.signature` stops working. Add the [redeem signature](#sign-the-redemption-with-the-paying-wallet) step, and upgrade to `@x1id/resolve` **0.22.0** for `x402RedeemMessage`. The request-bound reference is the only payment shape: a payment transaction with any other instruction, including a Memo, is refused.
</Callout>

<Callout type="info">
  x402 is only live on a deployment that has a pay-to address configured. Check `GET /v1/health` — its `x402` field tells you whether pay-per-call is accepted, and `rails` lists which payment rails are on.
</Callout>

### x402 headers (v2 names)

api.x1id.io speaks the x402 **v2** HTTP header names and still accepts the v1 ones:

| Direction                     | v2 header (preferred)    | v1 header (still accepted) | Contents                                                                                                                                                                                  |
| ----------------------------- | ------------------------ | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Request: your payment         | `PAYMENT-SIGNATURE`      | `X-PAYMENT`                | base64 JSON payment payload. On the X1-native rail: `{ x402Version, scheme: "exact", network, payload: { signature, payerSig } }` (the v2 `accepted: { scheme, network }` form works too) |
| Response: the `402` challenge | `PAYMENT-REQUIRED`       | (body only)                | base64 of the same `{ x402Version, accepts: [...] }` object that is also the `402` body                                                                                                   |
| Response: settled `200`       | `PAYMENT-RESPONSE`       | `X-PAYMENT-RESPONSE`       | base64 settlement receipt (both headers carry the same value)                                                                                                                             |
| Request: who is paying        | `X-Payer` (or `?payer=`) |                            | your wallet's base58 address. See below                                                                                                                                                   |

Send **one** payment header. If `PAYMENT-SIGNATURE` and `X-PAYMENT` are both present and differ, the request is rejected.

### Bind your payer with X-Payer on the first request

<Callout type="warn">
  **`X-Payer` is required.** Send `X-Payer: <your wallet>` (or `?payer=`) on the request that gets the `402`. Only then does the X1-native challenge carry a **request-bound reference** (`extra.reference`). Without it the `402` has `"code": "PAYER_REQUIRED"` and no reference.
</Callout>

With `X-Payer`, the X1-native `accepts[]` entry's `extra` holds:

* `feePayer`: your wallet. You pay your own fee; there is no sponsor.
* `reference`: a base58 address you add to your payment (see below). It binds the payment to **this exact request** (method, path, query and body), to the challenge, and to your wallet.
* `requestHash`: hex SHA-256 of the canonical request the reference was issued for. Optional to check.
* `redeemMessage`: the text your wallet signs to redeem the payment (see [below](#sign-the-redemption-with-the-paying-wallet)). It is `x402RedeemMessage(reference)`.

A live challenge, requested with `X-Payer`:

```json
{
  "x402Version": 2,
  "accepts": [{
    "scheme": "exact",
    "network": "solana:4SvBP3omtvcCVWdxq1zBY5cDp4wndjsT",
    "amount": "100000",
    "asset": "So11111111111111111111111111111111111111112",
    "payTo": "E2cELMtby3YYYbDdxamuGKWCyte3kwLywv19pEk2iUq6",
    "maxTimeoutSeconds": 60,
    "extra": {
      "feePayer": "H8FsKYhMyCN9brknY7tL4JXRbKZEjVh7XgrVmWcX1HiA",
      "reference": "S7k9Y5brdu1MBfV8WykKzu8KBkFi97ri5JUp5H4cTTC",
      "requestHash": "e96b43f9ae0ea38a788a748548527d328a67cb16271f64a99eb97e630f4a82bc",
      "redeemMessage": "x1id-x402-redeem:v1|api.x1id.io/dev-api\nreference:S7k9Y5brdu1MBfV8WykKzu8KBkFi97ri5JUp5H4cTTC"
    }
  }]
}
```

Redemption is bound to the wallet you named: only a payment whose `TransferChecked` authority **is** that wallet, which that wallet **signs**, and whose redemption carries that wallet's signature over `redeemMessage`, can redeem it. A later `X-Payer` cannot re-point it. A malformed `X-Payer` (not a base58 address) is **silently ignored**, as if you had not sent it, so double-check the value if you see `PAYER_REQUIRED`.

**Retry the identical request.** A payment for `GET /v1/resolve/alice` cannot be redeemed on `/v1/resolve/bob`, and a `POST /v1/gate/check` payment only works with the same body bytes. Path case matters: `@Alice` and `@alice` are different requests.

### Always set a compute-unit limit

<Callout type="warn">
  **X1 charges the transaction fee by the compute-unit limit you request, not the compute units you use**: about 10 lamports per requested CU plus a small base fee. A transaction with no `SetComputeUnitLimit` defaults to **200,000 CU per instruction, at least 0.002 XNT**, which is many times the price of the call. Always set the limit explicitly, and size it to what your transaction actually consumes.
</Callout>

What a payment really costs, measured on a live X1 mainnet payment (2026-10-11, wrapped-native asset):

| Payment transaction                                             | CU consumed | Limit requested | Fee (lamports) |
| --------------------------------------------------------------- | ----------- | --------------- | -------------- |
| CU limit + CU price + `TransferChecked` **+ reference account** | **6,719**   | 7,719           | **80,191**     |

The reference account adds about 180 CU, which keeps the network fee below the 0.0001 XNT resolve price.

**Simulate, then size the limit to what was consumed plus about 1,000 CU of headroom** (about 7,700 for the reference shape). Do not guess low: **a limit below what the transaction consumes makes it fail on chain, and you are still charged the fee.** Keep it at or under the server's cap of **40,000** CU.

### The payment transaction

The X1-native rail verifies a **fixed shape**:

1. `ComputeBudget::SetComputeUnitLimit`: sized from simulation. It must be first, and anything above 40,000 is rejected (`compute-limit-too-high`).
2. `ComputeBudget::SetComputeUnitPrice`: a small price (x1id's builders use 1 micro-lamport/CU; the cap is 5,000,000).
3. **One** SPL `TransferChecked` of at least the quoted `amount` of the quoted `asset` to `payTo`'s associated token account, signed by your wallet, with **`extra.reference` appended as one extra account, read-only and not a signer**. SPL Token ignores extra accounts on a single-owner transfer. Nobody signs with the reference; it is a hash, not a key.

<Callout type="info">
  **Explorers may mislabel the reference.** Because the `TransferChecked` carries one account more than a plain transfer, explorers and `jsonParsed` RPC output can decode it as a multisig transfer: the reference shows up as a multisig "signer" and your wallet as `multisigAuthority`. Neither is what happens on-chain. The reference is a read-only, non-signer account (the standard Solana Pay reference pattern), and the transfer authority is your ordinary wallet, approved by its normal transaction signature. A mainnet payment in this shape, for comparison: [`2UUMMij6…9UdRo`](https://explorer.fortiblox.com/tx/2UUMMij6tX5RGHqgjnLJoo2Y6PoCRpnq3Cqaf6fUs3Eu8HpQHPhsH7GuYMZZrZ8JtvJSsf8gNxcG9SEXEXz9UdRo).
</Callout>

**Allowed instructions.** Only ComputeBudget and SPL Token instructions are allowed (a Memo instruction gets `PAYMENT_LAYOUT_INVALID`), exactly one transfer may pay `payTo`, the reference must appear exactly once and only in the transfer, and Address Lookup Tables are refused. A multisig token authority is not supported.

The amount is checked twice: the `TransferChecked` amount, and the balance actually credited to `payTo`'s token account, which must also cover the price. A Token-2022 mint whose transfer fee leaves `payTo` short is refused, and so is a Token-2022 payment whose credited amount the RPC can't report.

**Wrap native XNT in a separate, earlier transaction.** The default `asset` is the wrapped-native mint (`So11111111111111111111111111111111111111112`). It has **9 decimals**, like XNT. If your wrapped balance is short, top it up first (create the ATA if needed, transfer XNT into it, `SyncNative`) and wait for it to confirm. Those instructions cannot go in the payment transaction: instruction 0 must be the compute-unit limit, and the wrap has to happen before the transfer.

```ts
import {
  ComputeBudgetProgram, Connection, PublicKey, TransactionMessage, VersionedTransaction,
} from "@solana/web3.js";
import { createTransferCheckedInstruction, getAssociatedTokenAddressSync } from "@solana/spl-token";
import { encodeBase58, parseX402Response, x402RedeemMessage } from "@x1id/resolve"; // 0.22.0+

const API = "https://api.x1id.io/v1/resolve/alice";
const connection = new Connection("https://rpc.mainnet.x1.xyz");
const payer = wallet.publicKey; // your wallet adapter / keypair

// 1. Ask for the challenge. X-Payer is required to get a reference.
const challenge = await fetch(API, { headers: { "X-Payer": payer.toBase58() } });
const parsed = parseX402Response(await challenge.json()); // status 402
const X1_MAINNET = "solana:4SvBP3omtvcCVWdxq1zBY5cDp4wndjsT"; // match exactly: a CDP rail is also "solana:..."
const req = parsed?.accepts.find((a) => a.network === X1_MAINNET);
if (!req?.extra.reference) throw new Error("no reference: send X-Payer"); // PAYER_REQUIRED
const mint = new PublicKey(req.asset);

// 2. Build the payment: [CU limit, CU price, TransferChecked + reference].
//    (Wrap XNT -> wSOL in an EARLIER transaction if your balance is short.)
const transfer = createTransferCheckedInstruction(
  getAssociatedTokenAddressSync(mint, payer),
  mint,
  getAssociatedTokenAddressSync(mint, new PublicKey(req.payTo), true), // payTo may be a PDA
  payer,
  BigInt(req.amount),
  9, // wrapped-native XNT has 9 decimals (read the mint for any other asset)
);
transfer.keys.push({ pubkey: new PublicKey(req.extra.reference), isSigner: false, isWritable: false });
const build = (units: number) => [
  ComputeBudgetProgram.setComputeUnitLimit({ units }),
  ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1 }),
  transfer,
];
const { blockhash } = await connection.getLatestBlockhash("finalized");
const compile = (units: number) => new VersionedTransaction(
  new TransactionMessage({ payerKey: payer, recentBlockhash: blockhash, instructions: build(units) }).compileToV0Message(),
);

// Simulate at the 40,000 cap, then request what it used + ~1,000 headroom
// (~7,700). Too low = failed tx, fee still charged.
const sim = await connection.simulateTransaction(compile(40_000), { sigVerify: false, replaceRecentBlockhash: true });
if (sim.value.err) throw new Error(`payment would fail: ${JSON.stringify(sim.value.err)}`);
const tx = compile(Math.min(40_000, sim.value.unitsConsumed! + 1_000));
const signature = await connection.sendTransaction(await wallet.signTransaction(tx));
await connection.confirmTransaction(signature, "finalized");

// 3. Sign the redemption with the SAME wallet that paid. The 402 carries the
//    message verbatim; the SDK rebuilds it so you can check it first.
const redeemMessage = x402RedeemMessage(req.extra.reference);
if (req.extra.redeemMessage !== redeemMessage) throw new Error("unexpected redeemMessage");
const payerSig = encodeBase58(await wallet.signMessage(new TextEncoder().encode(redeemMessage)));

// 4. Claim: the IDENTICAL request, both signatures in PAYMENT-SIGNATURE.
//    ({ reference: req.extra.reference, payerSig } instead of
//    { signature, payerSig } also works: the server finds the payment by its reference.)
const payment = btoa(JSON.stringify({
  x402Version: 2, scheme: "exact", network: req.network, payload: { signature, payerSig },
}));
const res = await fetch(API, { headers: { "X-Payer": payer.toBase58(), "PAYMENT-SIGNATURE": payment } });
// 200 + PAYMENT-RESPONSE header on success
```

The server only accepts a **finalized** payment, so wait for finality before you claim. A `402` with `PAYMENT_NOT_FINALIZED` means retry the same payment header shortly. With `{ reference }`, the server checks every finalized transaction that lists your reference and skips any that only mention it without being a valid payment, so another transaction naming your reference can't block you; if none is valid yet you get the same retryable `PAYMENT_NOT_FINALIZED`.

### Sign the redemption with the paying wallet

Your payment is public once it lands, signature and all. So the claim proves the caller is the payer: `payload.payerSig` is the base58 ed25519 signature, by the wallet bound with `X-Payer`, over `extra.redeemMessage`:

```text
redeemMessage = "x1id-x402-redeem:v1|api.x1id.io/dev-api" "\n" "reference:" reference
```

* Sign it with your wallet's `signMessage`. A raw signature over the UTF-8 bytes and one over a wallet's Solana off-chain message envelope (SIMD-0048) are both accepted.
* `@x1id/resolve` 0.22.0 exports `x402RedeemMessage(reference)` and the domain string `X402_REDEEM_DOMAIN`, so you can rebuild the message and compare it with the one in the `402`.
* The reference is single-use and already bound to the request and the payer, so a `payerSig` can't be reused on another challenge.
* A missing `payerSig` gets `REDEEM_SIGNATURE_REQUIRED`, and one that doesn't verify gets `REDEEM_SIGNATURE_INVALID`. **Neither consumes the payment:** sign and resend the same request with the same payment.

### How the reference is derived

You do not need to compute it, since the `402` gives it to you. To check it, or to confirm you are paying for the request you will send, the `@x1id/resolve` SDK exports `canonicalX402Request`, `x402RequestHash` and `deriveX402Reference`:

```text
canonical   = METHOD "\n" path "\n" query "\n" hex(SHA-256(body))
requestHash = hex(SHA-256(canonical))
reference   = base58(SHA-256("x1id-x402-ref:v1|api.x1id.io/dev-api" "\n" canonical "\n" nonce "\n" payer))
```

* **METHOD** is uppercased. Scheme, host and fragment are left out.
* **path**: each `/`-separated segment is percent-decoded, then re-encoded per RFC 3986 (letters, digits and `- . _ ~` stay literal; every other byte becomes `%XX`, uppercase). Trailing slashes are kept and case is preserved.
* **query**: split on `&`, `+` means a space, each key and value decoded and re-encoded, pairs sorted by key then value, duplicates kept, a bare `k` becomes `k=`.
* **body**: the exact bytes sent. GET, HEAD and empty bodies use SHA-256 of the empty string (`e3b0c442…7852b855`).
* **nonce** is the challenge's server-side nonce. It is not sent to you: the server computes the reference and puts it in `extra.reference`.

Worked example:

|             |                                                                                                                 |
| ----------- | --------------------------------------------------------------------------------------------------------------- |
| Request     | `GET https://api.x1id.io/v1/resolve/@Alice?fresh=1&chain=x1`                                                    |
| canonical   | `GET\n/v1/resolve/%40Alice\nchain=x1&fresh=1\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855` |
| requestHash | `9a408b21042932cda138cd88c8421e27cc23b699ab6780084dfa22269edfb6ce`                                              |
| nonce       | `00112233445566778899aabbccddeeff`                                                                              |
| payer       | `H8FsKYhMyCN9brknY7tL4JXRbKZEjVh7XgrVmWcX1HiA`                                                                  |
| reference   | `GxfSof9wYBDnnLUXR3ZqePRUoNN2etHiJYkkaUShVseQ`                                                                  |

### x402 error codes

A rejected payment gets another `402` carrying a fresh challenge, with the rejection's code in both `error` and `code` and an explanation in `message`, so you can branch on `error` and pay again from the same response:

| `code`                           | Meaning                                                                                                                                     |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `PAYER_REQUIRED`                 | The challenge was requested without `X-Payer`, so it has no reference.                                                                      |
| `PAYMENT_PAYLOAD_INVALID`        | The payment header is not base64 JSON of an x402 payment payload with `payload.signature` or `payload.reference`.                           |
| `REDEEM_SIGNATURE_REQUIRED`      | No `payload.payerSig`. Sign `extra.redeemMessage` with the paying wallet and resend; the payment was not consumed.                          |
| `REDEEM_SIGNATURE_INVALID`       | `payload.payerSig` is not the paying wallet's signature over `extra.redeemMessage`. The payment was not consumed.                           |
| `PAYMENT_NOT_FINALIZED`          | The payment is not finalized yet. Retry the identical request with the same payment header shortly.                                         |
| `PAYMENT_ALREADY_USED`           | This payment already bought a successful response. A replay of the same signature gets this code. Pay again against a fresh challenge.      |
| `REFERENCE_MISSING`              | No issued reference in the `TransferChecked`, or it is writable, a signer, repeated, or also in another instruction.                        |
| `REFERENCE_MISMATCH`             | The request changed (method, path, query, body or endpoint), or the payment carries two references.                                         |
| `REFERENCE_REUSED`               | A different payment carries a reference that has already paid for a successful call. Request a new challenge.                               |
| `REFERENCE_EXPIRED`              | Past the 600-second redeem window. Request a new challenge.                                                                                 |
| `MULTISIG_AUTHORITY_UNSUPPORTED` | The transfer authority is a multisig. Pay from a single-signer wallet.                                                                      |
| `PAYMENT_LAYOUT_INVALID`         | The transaction has an instruction outside the fixed shape (for example a Memo), a second transfer, or compute-budget values out of bounds. |

The [error reference](/docs/x1id/errors#x402-payments) lists every code the gateway returns, with its HTTP status and what to do.

A call whose handler fails (for example a `404`) does not consume your payment, so you can retry the same request with the same payment.

## Health

<Endpoint method="GET" path="/v1/health" />

Never gated. Reports liveness, whether x402 is enabled, and which RPC host and program the deployment reads.

```bash
curl https://api.x1id.io/v1/health
# → { "status": "ok", "service": "x1id-dev-api", "x402": true,
#     "rails": ["x1-native"], "rpc": "rpc.mainnet.x1.xyz",
#     "program": "8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P" }
```

## The platform endpoints

<Cards>
  <Card href="/docs/x1id/connect" icon="LogIn" title="Sign in with X1ID (Connect)" description="Challenge → sign → session. Prove a user controls their @handle or wallet." />

  <Card href="/docs/x1id/gate" icon="DoorClosed" title="Gate" description="Access control by on-chain attestation — verified-only allowlists behind one call." />

  <Card href="/docs/x1id/agents" icon="Bot" title="Agents" description="Resolve an agent @handle to its validated manifest, endpoints, and pay-to." />

  <Card href="/docs/x1id/universal-resolver" icon="Globe" title="Universal resolver" description="Resolve X1ID, .sol, and .eth names through one endpoint." />

  <Card href="/docs/x1id/events" icon="Webhook" title="Events & webhooks" description="The identity-event stream and the signed-webhook delivery contract." />

  <Card href="/docs/x1id/api" icon="Zap" title="Resolve API (public)" description="The keyless resolution endpoint — no /v1, no key." />
</Cards>
