# SDK reference
URL: /docs/x1id/sdk

> The @x1id/resolve TypeScript SDK — resolve @handles and X1NS names to addresses, reverse-resolve an address to its primary @handle, and read multi-chain records, all straight from chain.

# SDK reference

`@x1id/resolve` resolves **`@handles`** and **X1NS** names (`.x1` / `.xnt` / `.xen`) to addresses on X1, an address back to its primary `@handle`, and a handle's per-chain payment records. It reads accounts directly off chain over RPC — it never calls a hosted resolution API, so it works against any X1 RPC and keeps working if x1id.io itself goes away.

<LlmActions />

This page targets **`@x1id/resolve@0.22.0`** — the current npm `latest`.

<Callout type="info">
  **`@handle` resolution is live on X1 mainnet.** The handle registry program is deployed on X1 **mainnet** (`https://rpc.mainnet.x1.xyz`), and the same program at the same ID is also on X1 testnet (`https://rpc.testnet.x1.xyz`) for development. X1NS domain resolution (`.x1` / `.xnt` / `.xen`) is a separate, pre-existing system that exists on **X1 mainnet only** — against a testnet RPC an X1NS name is not found and `reverse()`'s X1NS fallback returns nothing. Point `rpcUrl` at the chain that holds the names you want; on an RPC where the registry is not deployed a handle is an honest `not-found`, never a fabricated address.
</Callout>

## Install

```bash
npm install @x1id/resolve@0.22.0
```

`@solana/web3.js` is an **optional** peer dependency — you only need it if you pass web3.js types around; the SDK talks to RPC directly and holds no keys.

## Loading the WASM module

Account derivation needs a real ed25519 on-curve check, which the SDK runs in a small Rust→WASM module rather than reimplementing in TypeScript — a hand-rolled JS version is the kind of thing that is subtly wrong for months, and this is code that loses money when it is wrong. `wasm` is therefore a **required** field on `ResolverConfig`; there is deliberately no JavaScript fallback. The package ships the module at the exported subpath `@x1id/resolve/wasm/x1_resolve_wasm.wasm`.

<Tabs items={['Node (ESM)', 'Browser / bundler']}>
  <Tab value="Node (ESM)">
    ```ts
    import { readFileSync } from 'node:fs';
    import { createRequire } from 'node:module';
    import { WasmResolver, createResolver } from '@x1id/resolve';

    const require = createRequire(import.meta.url);
    const wasmPath = require.resolve('@x1id/resolve/wasm/x1_resolve_wasm.wasm');
    const wasm = await WasmResolver.fromBytes(readFileSync(wasmPath));

    // testnet works the same: rpcUrl: 'https://rpc.testnet.x1.xyz'
    const x1id = createResolver({ rpcUrl: 'https://rpc.mainnet.x1.xyz', wasm });
    ```

    Node ≥ 20.6 can use `import.meta.resolve` instead of `createRequire`:

    ```ts
    import { fileURLToPath } from 'node:url';
    const wasmPath = fileURLToPath(import.meta.resolve('@x1id/resolve/wasm/x1_resolve_wasm.wasm'));
    ```
  </Tab>

  <Tab value="Browser / bundler">
    ```ts
    // Vite / Rollup / webpack 5 — the `?url` suffix hands you the asset URL
    import wasmUrl from '@x1id/resolve/wasm/x1_resolve_wasm.wasm?url';
    import { WasmResolver, createResolver } from '@x1id/resolve';

    const wasm = await WasmResolver.fromBytes(await (await fetch(wasmUrl)).arrayBuffer());

    // testnet works the same: rpcUrl: 'https://rpc.testnet.x1.xyz'
    const x1id = createResolver({ rpcUrl: 'https://rpc.mainnet.x1.xyz', wasm });
    ```

    Or copy `node_modules/@x1id/resolve/wasm/x1_resolve_wasm.wasm` into your static directory and `fetch('/x1_resolve_wasm.wasm')` it — that is what [x1id.io](https://x1id.io) does.
  </Tab>
</Tabs>

`WasmResolver.fromBytes()` calls `WebAssembly.instantiate(bytes, {})` on the bytes you hand it — the SDK never fetches the module itself. Load the copy that ships in the npm package you installed (or self-host that file); don't point at an unpinned CDN URL, which can serve a module from a different version than your JS. A resolver instance is single-threaded; create one per process (or per page) and reuse it.

### Browser requirements

* **CSP.** Compiling WebAssembly needs `'wasm-unsafe-eval'` in `script-src` (no `'unsafe-eval'` needed). `connect-src` must allow your X1 RPC (`https://rpc.mainnet.x1.xyz`), plus `https://api.x1id.io` if you call the REST API from the page. Server-side use needs no CSP change.
* **Serve the `.wasm` file as `application/wasm`.** The SDK instantiates from bytes, so the MIME type isn't checked by `fromBytes`, but serve it correctly anyway so CDNs and proxies don't mangle or block it.

### Next.js recipe

Written against Next.js 14/15 conventions; **not yet tested on Next.js 16**.

**Server (route handlers, server components) — recommended for a payment flow.** Use the Node runtime (the Edge runtime is untested), read the module from the package, and keep `@x1id/resolve` out of the server bundle so the `.wasm` file ships with the deployment:

```ts title="lib/x1id.server.ts"
import 'server-only';
import { readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { WasmResolver, createResolver, type Resolver } from '@x1id/resolve';

let resolver: Promise<Resolver> | undefined;

export function getResolver(): Promise<Resolver> {
  resolver ??= (async () => {
    const require = createRequire(import.meta.url);
    const bytes = await readFile(require.resolve('@x1id/resolve/wasm/x1_resolve_wasm.wasm'));
    const wasm = await WasmResolver.fromBytes(bytes);
    return createResolver({ rpcUrl: 'https://rpc.mainnet.x1.xyz', wasm });
  })();
  return resolver;
}
```

```js title="next.config.mjs"
export default {
  output: 'standalone',
  serverExternalPackages: ['@x1id/resolve'],
  outputFileTracingIncludes: {
    '/**': ['./node_modules/@x1id/resolve/wasm/*.wasm'],
  },
};
```

This is the Next 15 form. On Next 14 both keys live under `experimental`: `experimental.serverComponentsExternalPackages` and `experimental.outputFileTracingIncludes`.

**Browser (client components).** Copy the module into `public/` at install time, so it is served from your own origin at the version in your lockfile:

```json title="package.json"
{
  "scripts": {
    "postinstall": "cp node_modules/@x1id/resolve/wasm/x1_resolve_wasm.wasm public/x1_resolve_wasm.wasm"
  }
}
```

```ts title="lib/x1id.client.ts"
'use client';
import { WasmResolver, createResolver } from '@x1id/resolve';

export async function loadResolver() {
  const bytes = await (await fetch('/x1_resolve_wasm.wasm')).arrayBuffer();
  const wasm = await WasmResolver.fromBytes(bytes);
  return createResolver({ rpcUrl: 'https://rpc.mainnet.x1.xyz', wasm });
}
```

```js title="next.config.mjs (headers)"
export default {
  async headers() {
    return [
      { source: '/x1_resolve_wasm.wasm', headers: [{ key: 'Content-Type', value: 'application/wasm' }] },
      {
        source: '/:path*',
        headers: [
          {
            key: 'Content-Security-Policy',
            // One string. Merge these directives into your existing policy:
            // Next.js's own scripts may need a nonce or more sources.
            value: "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; connect-src 'self' https://rpc.mainnet.x1.xyz https://api.x1id.io",
          },
        ],
      },
    ];
  },
};
```

## createResolver

Create a resolver instance. Derivation and RPC reads happen lazily, on each call — nothing is fetched at construction time.

```ts
import { createResolver, WasmResolver } from '@x1id/resolve';

const wasm = await WasmResolver.fromBytes(/* module bytes, see above */);

const x1id = createResolver({
  rpcUrl: 'https://rpc.mainnet.x1.xyz',   // testnet works the same with rpc.testnet.x1.xyz
  wasm,
  cacheTtlMs: 30_000, // optional — this is the default
});
```

**Parameters (`ResolverConfig`):**

| Field             | Type           | Required | Description                                                                                                                                                                                        |
| ----------------- | -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rpcUrl`          | `string`       | Yes      | Any X1 RPC endpoint. Use `https://rpc.mainnet.x1.xyz` for `@handles` (`https://rpc.testnet.x1.xyz` for the testnet registry).                                                                      |
| `wasm`            | `WasmResolver` | Yes      | The loaded WASM module, from `WasmResolver.fromBytes()`. No JS fallback exists.                                                                                                                    |
| `fetchImpl`       | `typeof fetch` | No       | Custom transport for tests or proxies. Defaults to the global `fetch`.                                                                                                                             |
| `cacheTtlMs`      | `number`       | No       | Resolution cache TTL in milliseconds. Default `30_000`. Set `0` to disable caching (`resolve()` only).                                                                                             |
| `handleProgramId` | `string`       | No       | `@handle` registry program ID. Defaults to the canonical X1 deployment, `8JgnNWi24bq9uzfnT9XmkWxvaWMVgoEs9bu8QsHhLe1P`; override to point at a different deployment (e.g. a local test validator). |

**Returns:** `Resolver` — an object exposing `resolve`, `reverse`, `records`, and `clearCache`.

## resolve

Resolve a name to an address. `resolve` classifies its input **by shape** — a leading `@` means a handle, a recognized TLD suffix (`.x1` / `.xnt` / `.xen`) means an X1NS domain — and never falls back from one namespace to the other. Input shaped like both throws `ambiguous` rather than silently picking one.

```ts
resolve(input: string, opts?: { chain?: 'X1' | 'SOL' | 'ETH' | 'BTC' }): Promise<Resolved>
```

**Returns `Resolved`:**

| Field            | Type                              | Description                                                                                                                                                                                                                                                                                                                                             |
| ---------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `input`          | `string`                          | Exactly what was passed in, for display.                                                                                                                                                                                                                                                                                                                |
| `name`           | `string`                          | Canonical form of the name.                                                                                                                                                                                                                                                                                                                             |
| `namespace`      | `Namespace`                       | Which naming system answered. The union is `'handle' \| 'x1' \| 'xnt' \| 'xen' \| (string & {})` — the open string arm is a **customer-TLD label** (e.g. `'shib'` for `alices.shib`), so a UI must treat it as an arbitrary label, not an exhaustive enum. **Always render this before letting a user send** — see [the one rule](/docs/x1id/examples). |
| `address`        | `string`                          | The resolved address: base58 for X1/SOL, `0x`-hex for ETH, bech32 for BTC.                                                                                                                                                                                                                                                                              |
| `chain`          | `'X1' \| 'SOL' \| 'ETH' \| 'BTC'` | Chain the address belongs to.                                                                                                                                                                                                                                                                                                                           |
| `verification`   | `'verified' \| 'unverified'`      | Whether ownership of `address` was proved for this chain — see [Verification](#verification).                                                                                                                                                                                                                                                           |
| `verifications?` | `HandleVerification[]`            | Optional per-platform "verified by \[platform]" signals. The SDK's own on-chain `resolve()` does **not** populate this — read [attestations directly](/docs/x1id/verification).                                                                                                                                                                         |

```ts
const res = await x1id.resolve('@nike');
// {
//   input: '@nike', name: 'nike', namespace: 'handle',
//   address: 'E2cELMtby3YYYbDdxamuGKWCyte3kwLywv19pEk2iUq6',
//   chain: 'X1', verification: 'verified'
// }
```

<Callout type="error">
  **`@nike` is a held (reserved) name — don't send to it.** The address above is the X1ID treasury (`E2cELMtby3YYYbDdxamuGKWCyte3kwLywv19pEk2iUq6`), which holds reserved and premium names until they reach an owner through the marketplace. `verification: 'verified'` only means the treasury controls the account; it says nothing about the brand. The SDK carries **no** held flag. Before a payment, check the REST [`/quote/{name}`](/docs/x1id/api#price-quote) endpoint and refuse the send when `status` is `"held"`, and refuse any resolution whose address is the treasury. See [the pre-send checks](/docs/x1id/examples#step-4--check-before-you-sign).
</Callout>

### Tokenized handles

A handle can be minted into a capability **NFT**, after which *holding the NFT* is the authority and the registry's `owner` field stops moving with the name. `resolve()` handles this for you: for a tokenized handle it returns whoever currently holds the NFT **in their associated token account**, never the stale `owner` field. A handle whose NFT has been **burned** (mint supply 0) resolves to no one — `ResolveError { code: 'not-found', reason: 'nft-burned' }`, never the stale `owner`.

### The ambiguous case

<Callout type="warn">
  **This is the one behavior to get right.** `@jack` and `jack.x1` can be registered to **different people**. There is no fallback chain in this SDK from one namespace to the other — a fallback is exactly how those two identities get silently conflated, and the failure mode is a transfer that succeeds, to the wrong person. Input shaped like both (`@jack.x1`) throws `ambiguous` instead. Never catch it and pick one — ask the user which they meant.
</Callout>

## reverse

Look up the primary name for an address.

```ts
reverse(address: string): Promise<string | null>
```

Checked in this order, and returned as a bare string (the handle without the `@`) or `null`:

1. **On-chain `@handle` primary.** Returned only when all three trust rules hold: the handle it names is a live registry account; the pointer was set no earlier than the handle's current `registered_at` (so a pointer left from before a release + re-register doesn't count); and the address still holds authority (the `owner` field if untokenized, or an associated token account holding exactly 1 of the handle's NFT if tokenized).
2. **X1NS primary domain, as a fallback.** Only when there is no valid handle primary. The X1NS record stores the domain *account*, not its label, so the result is `"<domainAccount>.<tld>"` — not `"jack.x1"`.
3. **`null`** otherwise. "No primary name" is a normal state, not an error — don't treat it as a failure.

```ts
const name = await x1id.reverse('E2cELMtby3YYYbDdxamuGKWCyte3kwLywv19pEk2iUq6');
// null — reverse() returns the wallet's PRIMARY name (set via set_primary),
// or null when none is set. resolve() and reverse() are NOT strict inverses:
// this is the X1ID treasury, which holds @nike (and other reserved names)
// but has set no primary, so it reverses to null.
// A wallet that HAS set a primary returns the bare handle, e.g. "nike".

// Telling the two shapes apart: a handle result never contains a dot.
const display = name === null ? null : name.includes('.') ? name : `@${name}`;
```

A pointer that fails any rule is treated as absent (falls through to X1NS, then `null`), never as an error. Only a malformed *address* throws (`ResolveError { code: 'unrecognized' }`).

## records

Every per-chain payment record of a `@handle`, with the [staleness rule](/docs/x1id/records#the-staleness-rule) already applied: records left behind by a previous registration of the same name — or by a previous holder when a capability NFT changed hands in a **native marketplace sale** — come back flagged `stale` with `verified` forced false. (A **plain** NFT transfer stamps nothing on chain and is not covered — see the caveat under [the staleness rule](/docs/x1id/records#the-staleness-rule).)

```ts
records(input: string): Promise<readonly HandleRecord[]>
```

```ts
const all = await x1id.records('@nike');
// HandleRecord[]: { account, handle, coinType, chain, value, address,
//                   verified, updatedAt, stale }  ([] when none are set)

import { liveRecords } from '@x1id/resolve';
const live = liveRecords(all); // the ones that belong to the current owner
```

`@handle` inputs only — X1NS domains keep their addresses elsewhere. Throws `not-found` for an unregistered handle, `unrecognized` for a non-handle input. See [Address records](/docs/x1id/records) for reading and writing the full record family.

## clearCache

```ts
x1id.clearCache();
```

`resolve` results are cached per `(namespace, name, chain)` for `cacheTtlMs` (default 30 s); `reverse` and `records` are not cached. Call `clearCache()` after a registration or transfer you know just happened on chain, so a stale result doesn't linger.

<Callout type="warn">
  **Re-resolve, uncached, right before signing a payment.** A name can change hands inside the cache window. `resolve()` has no per-call cache bypass, so for the final resolve before you sign a send either call `clearCache()` first or keep a second resolver created with `cacheTtlMs: 0` for that one call. A cached result is fine for display while the user types; cache a positive result for at most 30 s, and a not-found or a reverse label for at most 30–60 s.
</Callout>

<Callout type="warn">
  **The SDK is lease-unaware.** A [lease-to-own](/docs/x1id/faq#lease-to-own) handle past its paid-through date still resolves, with no lease signal on `Resolved` (the hosted `/v1/resolve` is the same). Before sending funds, read the free REST [`/resolve/{name}`](/docs/x1id/api), which adds `lease: { expires_at, lapsed }` for a leased handle, and refuse when `lapsed` is `true`.
</Callout>

## Multi-chain resolution

A name can carry addresses for more than one chain. Request the one you want with `opts.chain`:

```ts
await x1id.resolve('@jack', { chain: 'X1' });  // default — the handle's authority
await x1id.resolve('@jack', { chain: 'SOL' }); // X1 shares Solana's ed25519 key format
await x1id.resolve('@jack', { chain: 'ETH' }); // the handle's ETH record, if one is set
await x1id.resolve('@jack', { chain: 'BTC' }); // the handle's BTC record, if one is set
```

For **X1/SOL** the SDK returns the handle's current authority. For **ETH/BTC** it reads the handle's per-chain `Record` accounts; a handle without a live record for the requested chain throws `ResolveError { code: 'no-record-for-chain' }` — an explicit "no record", never a wrong address on the wrong chain. Every record is judged against the [universal staleness rule](/docs/x1id/records#the-staleness-rule) structurally.

## Verification

| `verification` | Meaning                                                                                                                                                                                                                                                              |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verified`     | Control of the address was proved: the address owns the `@handle` registry account, or — for a tokenized handle — holds its NFT **in its associated token account** (the only place the registry recognises as the name's authority).                                |
| `unverified`   | Ownership was not proved for that chain (X1NS), or — for a tokenized handle — the NFT is held outside the holder's ATA (an auxiliary account or escrow): that wallet controls the token but cannot act for the name until it is back in its ATA. **Show a warning.** |

This is per-chain control. The separate **"verified by \[platform]"** blue-tick signal (verified X, GitHub, domain…) is covered in [Reading verified identity](/docs/x1id/verification).

## Beyond resolution: the full helper surface

`@x1id/resolve` is not only a resolver — the same package ships the instruction builders, sign-in, gating, and cross-namespace helpers that app.x1id.io and the developer platform are built on. Every builder is transport-neutral (`{ programId, keys, data }`, no `@solana/web3.js` import) and holds no keys; you sign and relay. The groups below are the ones the pre-mainnet surface adds on top of `resolve` / `reverse` / `records`.

### Namespaces and scoped names

A launched customer TLD (`.shib`) is an on-chain `Namespace` account at `["namespace", tld]`. A name registered under it — `alices.shib` — is an always-tokenized `Handle` at the **two-seed** PDA `["handle", tld, name]`, a *different* account from the bare `@alices` at `["handle", name]`, so the two can have different owners. The `scoped` helpers are the SDK last-mile for these (feature #8484/#8485):

```ts
import { scopedTldCandidate, namespaceIsActive, makeScopedHandleDeriver } from '@x1id/resolve';

// Shape-only classifier (no RPC): is this a `name.tld` scoped candidate?
const cand = scopedTldCandidate('alices.shib'); // → { sub: 'alices', tld: 'shib' } | null

// Is a fetched `Namespace` account live? (byte-reader; caller checks program ownership)
const active = namespaceIsActive(namespaceAccountData); // → boolean

// The one PDA derivation scoped resolution needs — `["handle", tld, name]` —
// injected (web3.js) because this package never hand-rolls curve math:
const deriver = await makeScopedHandleDeriver();
const handlePda = await deriver.scopedHandleKey('shib', 'alices', programId);
```

`createResolver().resolve()` answers scoped names directly — `resolve('alices.shib')` returns `namespace: 'shib'` once that TLD is a launched, `Active` namespace; the owner/tokenization/verification read reuses the bare-handle authority path. The low-level `WasmResolver` also exposes `deriveNamespaceAccount(label, programId)` for `["namespace", label]`.

### Subnames (and subnames under a scoped handle)

A **subname** is a label under a parent handle — `pay` under `alice` → `pay.alice`. Subnames branch one further level, so `pay.alice` can carry its own `deep.pay.alice` **sub-subname** (see [What you can do](/docs/x1id/features#subnames)). A subname has no owner of its own: the parent's current authority controls it. Because a scoped name is itself a `Handle`, a subname can also hang under one, giving `pay.alices.shib`. Subname resolution enforces a **two-layer** staleness rule (parent-vs-subname, then record-vs-subname), so always read through `resolveSubnameRecords`:

```ts
import {
  buildCreateSubnameIx, buildRevokeSubnameIx,
  buildCreateSubnameRecordIx, buildUpdateSubnameRecordIx, buildCloseSubnameRecordIx,
  fetchSubnames, resolveSubnameRecords, subnameIsLive, decodeSubname,
} from '@x1id/resolve';

// Create `pay.alice` (rent-only, parent-authority-gated):
const ix = buildCreateSubnameIx({ programId, payer, owner, parent, subname, label: 'pay' });

// Resolve a subname's LIVE records — the ONLY safe entry point (both layers enforced):
const live = await resolveSubnameRecords(rpc, programId, subnameAccount, subname, parentRegisteredAt);
```

PDA derivation (`["subname", parentHandlePda, label]`, and records at `["record", subnamePda, coinType]`) is done with your runtime, exactly as the module docs show — this package does not hand-roll `find_program_address`.

### Sign in (Connect)

The `signin` helpers are the client side of [X1ID Connect](/docs/x1id/connect) — a domain-bound challenge, a wallet signature, verification, and the address→`@handle` identity, in one call:

```ts
import { signInWithX1ID, createSignInChallenge, verifySignIn, resolveIdentity } from '@x1id/resolve';

const identity = await signInWithX1ID({
  rpcUrl, wasm,                 // IdentityConfig: how to resolve the handle
  address,                      // the connected wallet (base58)
  signMessage,                  // wallet.signMessage: (bytes) => Promise<64-byte sig>
  domain: 'yourapp.com',
  requireHandle: true,          // default — "Sign in with X1ID" implies one; false allows handle-less
});
// → SignInIdentity: { address, handle, verified…, challenge, nonce, issuedAt, expiresAt }
```

`createSignInChallenge` / `parseSignInChallenge` / `verifySignIn` are the split primitives if you want the backend to verify a challenge your frontend had signed; `resolveIdentity(cfg, address)` is the identity read on its own. This is a server-less proof your own backend verifies; it doesn't use the hosted API.

**Against the hosted Connect API**, use `signInWithWallet` (SIWS, then a never-sent memo transaction, then v2 `signMessage`, falling back automatically) or `signInWithSolana` (SIWS only, allowlisted domains only). They sign the server's audience-bound strings verbatim and return a Bearer session. The memo-transaction path is what makes Backpack, the X1 wallet, and Ledger work.

```ts
import { signInWithWallet } from '@x1id/resolve';

const { method, ...session } = await signInWithWallet({
  wallet,                                         // Wallet Standard wallet
  apiBase: `${window.location.origin}/api/x1id`,  // your backend proxy, which adds X-API-Key
  apiKey: 'server-held',                          // placeholder; the real key never reaches the browser
  rpcUrl: 'https://rpc.mainnet.x1.xyz',
});
```

<Callout type="warn">
  **0.21.0 is required for hosted sign-in.** The API now accepts only audience-bound strings, and `signinWithTx` / `buildVerifyTxChallenge` from SDK 0.20.x and earlier throw on the bound memo. See [Sign in with X1ID](/docs/x1id/connect#with-the-sdk) for the full flow, the SIWS domain allowlist, and the backend proxy.
</Callout>

### Gate on verified identity

The `gate` helpers test a handle's on-chain attestations against a policy — read it yourself over RPC, or call the hosted [Gate endpoint](/docs/x1id/gate):

```ts
import { attestationsPassGate, handlePassesGate, GateKind, checkGate, requireGate } from '@x1id/resolve';

// A Gate is "any-verified" | { kind } | { anyOf: number[] }, with named kinds:
const gate = { anyOf: [GateKind.x, GateKind.github] }; // dns,social,x,discord,github,telegram

// Fetch + test in one RPC call:
const { passed, attestations } = await handlePassesGate(rpc, programId, handleAccount, registeredAt, recordsClearedAt, gate);

// …or the hosted client (keyed or x402). Server-side only: never put the key in browser code.
const res = await checkGate({ apiKey: process.env.X1ID_API_KEY!, baseUrl }, { handle: '@jack' }, 'any-verified');
```

Stale attestations never count toward a gate.

### Pay per call (x402)

Without an API key, the [`/v1` platform](/docs/x1id/platform#pay-per-call-with-x402) answers `402` and is paid per call. The SDK holds no keys and builds no transactions, but it parses the challenge and builds the message your wallet signs to redeem the payment:

```ts
import { encodeBase58, parseX402Response, x402RedeemMessage, X1_MAINNET_NETWORK } from '@x1id/resolve';

const res = await fetch(url, { headers: { 'X-Payer': wallet.publicKey.toBase58() } });
const req = parseX402Response(await res.json())?.accepts.find((a) => a.network === X1_MAINNET_NETWORK);
// …pay req.amount of req.asset to req.payTo with req.extra.reference on the TransferChecked → `signature`

const message = x402RedeemMessage(req!.extra.reference!); // === req.extra.redeemMessage
const payerSig = encodeBase58(await wallet.signMessage(new TextEncoder().encode(message)));
const payment = btoa(JSON.stringify({ x402Version: 2, scheme: 'exact', network: req!.network, payload: { signature, payerSig } }));
await fetch(url, { headers: { 'X-Payer': wallet.publicKey.toBase58(), 'PAYMENT-SIGNATURE': payment } });
```

**Breaking in 0.22.0:** api.x1id.io requires `payload.payerSig` on every redemption (`REDEEM_SIGNATURE_REQUIRED` without it), and `buildX402Requirements` no longer takes a memo; the request-bound reference is the only payment shape. `X402_REDEEM_DOMAIN` is the message's domain prefix, and `canonicalX402Request` / `x402RequestHash` / `deriveX402Reference` let you check a reference. The full client, with the payment transaction, is on [Pay-per-call with x402](/docs/x1id/platform#pay-per-call-with-x402).

### Universal resolution (.sol / .eth)

`createUniversalResolver` wraps the native resolver with optional SNS (`.sol`) and ENS (`.eth`) adapters behind one `resolve`, mirroring the hosted [universal resolver](/docs/x1id/universal-resolver). Each external namespace is gated off unless you supply its RPC/config:

```ts
import { createUniversalResolver } from '@x1id/resolve';

const uni = createUniversalResolver({
  native: x1id,                 // the createResolver() instance
  sns: { rpcUrl, deriver },     // omit to gate .sol off
  ens: { rpcUrl: ETH_RPC_URL }, // omit to gate .eth off
});

await uni.resolve('vitalik.eth');  // throws `not-configured` if that namespace is disabled
uni.detect('jack.sol');            // classify by shape → namespace | null
uni.status();                      // which namespaces are live vs config-gated
```

## Should I even try to resolve this?

Not every recipient-field input is a name — most of the time it's a pasted address. Use `looksLikeName()` to decide whether to attempt resolution at all, so a pasted base58 address never surfaces a spurious "invalid handle" error:

```ts
import { looksLikeName } from '@x1id/resolve';

if (looksLikeName(value)) {
  // "@jack", "jack.x1", "jack.xnt", "jack.xen" — attempt resolution
  resolveAndShowRecipient(value);
} else {
  // Treat as a raw address instead — validate/checksum it directly
  validateRawAddress(value);
}
```

`looksLikeName()` is a pure, synchronous shape check — no RPC, never throws. It returns `true` for anything starting with `@`, or ending in a recognized TLD, regardless of whether that name is actually registered.

## Errors

`resolve`, `reverse` and `records` throw `ResolveError`, never a bare string — so a UI can branch on `.code` instead of parsing a message:

```ts
import { ResolveError } from '@x1id/resolve';

try {
  const res = await x1id.resolve(userInput);
} catch (e) {
  if (e instanceof ResolveError) {
    console.log(e.code);   // e.g. "not-found", "ambiguous", "invalid-handle"
    console.log(e.input);  // the original input that caused the error
  }
}
```

See the [Error reference](/docs/x1id/errors) for the full list of `ResolveErrorCode` values and what a UI should do about each.

## WasmResolver

The loaded module also exposes the low-level derivation primitives, if you need to derive accounts yourself (for instance to build a write instruction — see [Register a handle](/docs/x1id/register)):

```ts
class WasmResolver {
  static fromBytes(bytes: BufferSource): Promise<WasmResolver>;
  normalizeHandle(raw: string): string | null;
  deriveHandleAccount(canonical: string, programId: Uint8Array): Uint8Array | null;        // ["handle", name]
  deriveHandlePrimaryAccount(owner: Uint8Array, programId: Uint8Array): Uint8Array | null; // ["primary", owner]
  deriveX1nsAccount(label: string, tld: 'x1' | 'xnt' | 'xen'): Uint8Array | null;
  deriveNamespaceAccount(label: string, programId: Uint8Array): Uint8Array | null;         // ["namespace", label]
  derivePrimaryAccount(owner: Uint8Array): Uint8Array | null;                              // X1NS primary-domain record
  deriveAssociatedTokenAccount(owner: Uint8Array, mint: Uint8Array): Uint8Array | null;
}
```

Also exported: `parseName`, `looksLikeName`, `normalizeHandle`, `namespaceLabel`, `encodeBase58`, `decodeBase58`, `decodeBase58_32`, `CHAIN_COIN_TYPE`, and the types `Resolved`, `Namespace`, `Chain`, `Verification`, `ResolveErrorCode`.

## Next

<Cards>
  <Card href="/docs/x1id/examples" icon="BookOpen" title="Add @handle to a send flow" description="The full wallet-integration walkthrough, and the non-negotiable 'show the namespace before you send' rule." />

  <Card href="/docs/x1id/records" icon="Link" title="Address records" description="Read multi-chain records and verify ETH / BTC / SVM addresses." />

  <Card href="/docs/x1id/verification" icon="BadgeCheck" title="Verified identity" description="Read the blue-tick: isVerified, verifiedPlatforms, fetchAttestations." />

  <Card href="/docs/x1id/errors" icon="TriangleAlert" title="Error reference" description="Every ResolveError code and what a UI should do." />
</Cards>
