# Wallet integration
URL: /docs/x1id/examples

> Add @handle and X1NS support to a wallet's send flow end to end — what to show for each outcome resolve() can return, and the non-negotiable rule that a name is never just an address.

# Add @handle support to a send flow

This walks through wiring `@x1id/resolve` into a wallet's recipient field. The live reference integration is the **X1ID app** itself — [app.x1id.io](https://app.x1id.io) uses exactly this SDK for its own resolution and reverse lookup. It covers the part that's easy to get wrong: what to show the user for each thing `resolve()` can do, so a mistyped or ambiguous name never turns into a transfer to the wrong recipient.

<LlmActions />

<Callout type="info">
  **`@handle` resolution is live on X1 mainnet.** The examples below point `rpcUrl` at X1 mainnet; the same registry at the same program ID is also on X1 testnet (`https://rpc.testnet.x1.xyz`) for development. X1NS `.x1` / `.xnt` / `.xen` domain resolution — a separate, older system — exists on mainnet only. This page targets **`@x1id/resolve@0.21.0`**.
</Callout>

## The setup

Most wallets already have a send screen with a recipient input. The only new piece: before that input is treated as an address, check whether it *looks like a name*, and if so, resolve it before rendering a recipient.

```ts
import { createResolver, WasmResolver, ResolveError, looksLikeName } from '@x1id/resolve';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';

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 });
```

(For a browser build, swap the WASM-loading block for the `fetch`-based one in the [SDK reference](/docs/x1id/sdk#loading-the-wasm-module).)

## Step 1 — decide whether to resolve at all

A recipient field gets two kinds of input: raw addresses (pasted or scanned) and names. Don't run a raw address through `resolve()` — it'll just throw `unrecognized`. Use `looksLikeName()` as the branch point:

```ts
function onRecipientInputChange(value: string) {
  if (looksLikeName(value)) {
    resolveRecipient(value); // → Step 2
  } else {
    // Treat as a raw address: run your existing validation/checksum path
    setRecipient(validateRawAddress(value));
  }
}
```

## Step 2 — resolve and shape the result

This is the core of the integration. `resolve()` either returns a `Resolved` or throws a `ResolveError` with a `code` your UI can switch on — there's no bare-string error to match against.

```ts
/** What the recipient field renders. The address is never shown alone. */
interface Recipient {
  address: string;
  display: string;   // "@jack" or "jack.x1" — the user must see which one they're paying
  verified: boolean; // false → show a warning before the user can send
}

type ResolveOutcome = { recipient: Recipient } | { hint: string };

async function resolveRecipient(input: string): Promise<ResolveOutcome> {
  try {
    const r = await x1id.resolve(input);
    return {
      recipient: {
        address: r.address,
        display: r.namespace === 'handle' ? `@${r.name}` : r.name,
        verified: r.verification === 'verified',
      },
    };
  } catch (e) {
    if (e instanceof ResolveError) {
      switch (e.code) {
        case 'not-found':           return { hint: 'No name found' };
        case 'ambiguous':           return { hint: 'Type either @jack or jack.x1 — not both' };
        case 'invalid-handle':      return { hint: 'Handles allow letters, digits and hyphens only' };
        case 'invalid-domain':      return { hint: "That doesn't look like a valid domain" };
        case 'no-record-for-chain': return { hint: 'This name has no address on the selected chain' };
        case 'rpc-error':           return { hint: 'Network issue — try again' };
        default:                    return { hint: 'Could not resolve that name' };
      }
    }
    throw e; // an unexpected, non-ResolveError failure — don't swallow it
  }
}
```

## Step 3 — the outcomes

```ts
for (const input of ['@nike', 'jack.x1', '@jack.x1', 'not a name']) {
  console.log(input, '→', await resolveRecipient(input));
}
// "@nike"      → { recipient: { address: "E2cELM…", display: "@nike",   verified: true  } }  ← held by the treasury: Step 4 refuses it
// "jack.x1"    → { recipient: { address: "9pXw…", display: "jack.x1", verified: false } }  (mainnet RPC)
// "@jack.x1"   → { hint: "Type either @jack or jack.x1 — not both" }
// "not a name" → never reaches resolveRecipient — looksLikeName() returns false first
```

* **Resolved, verified** — a handle resolved and control of the address was proved (the owner holds the registry account, or — for a tokenized handle — holds its NFT in their ATA). Show the recipient, then run the [pre-send checks](#step-4--check-before-you-sign) before enabling send. `verified` says who **controls** the address, not who the name **represents**: `@nike` above is `verified` because the X1ID treasury holds it as a reserved name.
* **Resolved, unverified** — an X1NS domain resolved to its owner, or a tokenized handle whose NFT sits outside the holder's ATA. Show the recipient, but surface the distinction — don't hide it behind the same confident UI as a verified result.
* **Not found / invalid** — `not-found`, `invalid-handle`, `invalid-domain`: nothing to render. Show an inline hint, leave send disabled, don't clear what the user typed.
* **Ambiguous** — never guess.

<Callout type="warn">
  **The one case that must never silently pick a side.** `@jack` and `jack.x1` can belong to different people. Input shaped like both — `@jack.x1` — throws `ambiguous` instead of the SDK guessing. Your UI must not guess either: don't fall back to "try the handle first, then the domain." Ask the user to disambiguate.
</Callout>

## Step 4 — check before you sign

What a user sees while typing can come from cache, and some names resolve to an address you must not pay. Right before you ask the wallet to sign a send, run these checks against fresh chain state and **refuse the send** if any fails:

1. **Re-resolve uncached.** The SDK caches `resolve()` for 30 s by default and has no per-call bypass, so keep a second resolver with `cacheTtlMs: 0` (or call `clearCache()` first) for the final resolve. If the address differs from the one the user confirmed, show the new one and ask again.
2. **Refuse held (reserved) names.** Brands, tickers, well-known words and a curated set of short names are registered to the **X1ID treasury** (`E2cELMtby3YYYbDdxamuGKWCyte3kwLywv19pEk2iUq6`) until they reach an owner through the marketplace. They resolve normally, to the treasury, and neither the SDK nor `/v1/resolve` flags them. Check the free REST [`/quote/{name}`](/docs/x1id/api#price-quote): `status: "held"` means treasury-held. Also refuse any resolution whose address is the treasury.
3. **Refuse lapsed leases.** The SDK and `/v1/resolve` are lease-unaware: a [lease-to-own](/docs/x1id/faq#lease-to-own) handle past its paid-through date still resolves to the current holder, and can be reclaimed by someone else after its grace period. The free REST [`/resolve/{name}`](/docs/x1id/api) adds `lease: { expires_at, lapsed }` for a leased handle; refuse when `lapsed` is `true`.

```ts
const TREASURY = 'E2cELMtby3YYYbDdxamuGKWCyte3kwLywv19pEk2iUq6'; // Config.treasury (immutable)
const fresh = createResolver({ rpcUrl: 'https://rpc.mainnet.x1.xyz', wasm, cacheTtlMs: 0 });

type PreSend = { ok: true; address: string } | { ok: false; reason: string };

async function checkBeforeSend(input: string, chain: 'X1' | 'SOL' | 'ETH' | 'BTC' = 'X1'): Promise<PreSend> {
  const r = await fresh.resolve(input, { chain }); // uncached; throws ResolveError as in Step 2
  if (r.address === TREASURY) return { ok: false, reason: 'This name is held by X1ID and has no owner yet.' };

  // The REST checks take top-level @handles only. Scoped names (name.tld) and
  // subnames (pay.alice) are resolved by the SDK above; /quote rejects them.
  const topLevelHandle = r.namespace === 'handle' && !r.name.includes('.');
  if (topLevelHandle) {
    const base = 'https://api.x1id.io';
    const name = encodeURIComponent(r.name);
    const [quote, rest] = await Promise.all([fetch(`${base}/quote/${name}`), fetch(`${base}/resolve/${name}?fresh=1`)]); // fresh: skip the ~20 s answer cache
    // Fail closed: if a check can't be read (429, 5xx), don't send.
    if (!quote.ok || !rest.ok) return { ok: false, reason: 'Could not verify this name right now. Try again.' };
    const q = await quote.json();
    const res = await rest.json();
    if (q.status === 'held') return { ok: false, reason: 'This name is reserved and held by X1ID.' };
    if (res.lease?.lapsed === true) return { ok: false, reason: "This name's lease has lapsed." };
  }
  return { ok: true, address: r.address };
}
```

The REST checks cover top-level `@handles` only: `/quote` rejects a dotted name, so for scoped names (`name.tld`) and subnames use the SDK (the uncached resolve plus the treasury check above) and don't refuse them just for being dotted. An unregistered name of any kind throws `not-found` from the SDK, so it never reaches a send. The REST API is rate-limited per IP (100 requests/s, burst 200; see [Rate limits](/docs/x1id/api#rate-limits)), so a backend that runs these checks for many users should expect `429` or `503` under load, wait the `Retry-After` seconds, and keep failing closed.

### Edge cases

| Case                           | SDK `resolve()`                   | REST `/resolve`                | `/v1/resolve`             | Do                                 |
| ------------------------------ | --------------------------------- | ------------------------------ | ------------------------- | ---------------------------------- |
| Unregistered                   | throws `not-found`                | `404`                          | `404`                     | show "No name found"               |
| Held / reserved (treasury)     | resolves to the treasury, no flag | same                           | same                      | `/quote` `status: "held"` → refuse |
| Leased, past paid-through date | resolves, no lease signal         | resolves, `lease.lapsed: true` | resolves, no lease signal | refuse when `lapsed`               |
| Bare `jack` (no `@`)           | throws (needs `@`)                | treated as a handle            | treated as a handle       | require the `@` in your UI         |
| `@jack.x1`                     | throws `ambiguous`                | `409`                          | `400 AMBIGUOUS`           | ask the user which one             |
| X1NS name on testnet           | not found                         | —                              | —                         | X1NS is mainnet-only               |

### Trust fields

| Field                        | Where                                        | What it means                                                                                                                                                                                                                    | What it does **not** mean                                                                            |
| ---------------------------- | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `namespace`                  | SDK, REST `/resolve`, `/v1/resolve`          | which naming system answered (`handle`, `x1`, `xnt`, `xen`, or a customer TLD)                                                                                                                                                   | — always show it before a send                                                                       |
| `verification: "verified"`   | SDK                                          | control of the address is proved: it owns the registry account, or holds the handle's NFT in its ATA                                                                                                                             | that the name belongs to the person or brand it reads as (held names are `verified` to the treasury) |
| `verification: "unverified"` | SDK                                          | ownership not proved for that chain; X1NS results are always `unverified`                                                                                                                                                        | — show a warning                                                                                     |
| `source`                     | `/v1/resolve`                                | the SDK's `verification`, renamed: `on-chain-verified` / `on-chain-unverified`                                                                                                                                                   | same caveats as above                                                                                |
| `verification: "owner"`      | REST `/resolve`                              | a constant label on every result                                                                                                                                                                                                 | not a trust grade                                                                                    |
| `verifications[]`            | REST `/resolve`, `/v1/resolve`, `/v1/verify` | "verified by x1id" badges: off-chain reviews signed by X1ID's attestor key, only while live for the current owner; REST forces `verified: false` (with `holder_mismatch` and a `caveat`) when a tokenized handle's NFT has moved | not a trustless proof, and not a reason to skip any pre-send check                                   |
| `lease`                      | REST `/resolve` only                         | `{ expires_at, lapsed }` for a leased handle; absent for permanent handles                                                                                                                                                       | grace period isn't modelled; `lapsed` is the paid-through fact                                       |
| `status`                     | REST `/quote`                                | `available`, `held` (treasury-held), or `taken`                                                                                                                                                                                  | `taken` doesn't tell you who owns it — resolve for that                                              |

## Step 5 — the non-negotiable rule

Regardless of which outcome fires, one rule holds across the whole integration: **never silently fill a recipient address into the send form.** Always show the resolved `namespace` next to the address before the user can confirm — `@jack` and `jack.x1` reading as the same "recipient" in your UI is exactly the ambiguity the SDK refuses to resolve on your behalf.

```ts
function recipientConfirmation(recipient: Recipient) {
  return {
    heading: `Sending to ${recipient.display}`,  // always the name, never just the address
    subheading: recipient.address,
    warning: recipient.verified ? null : "This address hasn't been verified for this chain.",
    sendEnabled: true,
  };
}
```

## Step 6 — multi-chain sends

If your wallet sends on more than one chain, pass `chain` through and handle `no-record-for-chain` as its own outcome rather than falling back to an X1 address on the wrong chain:

```ts
async function resolveForChain(input: string, chain: 'X1' | 'SOL' | 'ETH' | 'BTC') {
  try {
    return { recipient: await x1id.resolve(input, { chain }) };
  } catch (e) {
    if (e instanceof ResolveError && e.code === 'no-record-for-chain') {
      return { hint: `@${input.replace(/^@/, '')} has no address on ${chain} yet` };
    }
    throw e;
  }
}
```

For X1/SOL the SDK returns the handle's authority; for ETH/BTC it reads the handle's per-chain [records](/docs/x1id/records), with the staleness rule enforced. A handle with no live record for the requested chain throws `no-record-for-chain` — never a wrong or coerced address.

## Step 7 — show `@name` next to an address (reverse)

The other half of a naming system: anywhere your wallet prints an address — an activity feed, a contact list, the "from" line on an incoming transfer — you can print the `@handle` its owner declared as primary instead.

```ts
async function labelFor(address: string): Promise<string> {
  const name = await x1id.reverse(address); // "nike" | "9pXw….x1" | null
  if (name === null) return shorten(address);      // no primary set — show the address
  if (name.includes('.')) return shorten(address); // X1NS fallback carries no label; keep the address
  return `@${name}`;                               // on-chain primary handle
}
```

Two things to keep straight:

* A primary is **set by the owner and validated at read time**, not kept in sync by the program. If the handle is transferred, released, or its NFT sold, `reverse()` simply starts returning `null` for the old address — it never returns a name that address no longer controls.
* A reverse label is a **display hint, not a recipient.** If a user wants to *send* to `@nike`, run it through `resolve()` — don't reuse an address you previously reverse-labelled.

## Full reference

The live reference for everything above is the X1ID app at [app.x1id.io](https://app.x1id.io) — its recipient handling, handle pages and "primary for …" badges are built on this SDK. For the method-by-method API this example builds on, see the [SDK reference](/docs/x1id/sdk); for a copy-paste resolver singleton and a \~30-line React `<AddressName>`, the package [README on npm](https://www.npmjs.com/package/@x1id/resolve).

## Next

<Cards>
  <Card href="/docs/x1id/sdk" icon="Code" title="SDK reference" description="Every method, option, and the WASM loader." />

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

  <Card href="/docs/x1id/records" icon="Link" title="Address records" description="Read and verify the multi-chain records resolution reads." />
</Cards>
