# Quickstart
URL: https://docs.arcns.io/quickstart/

> Install @arcns/resolve, create one resolver, resolve @nike, nike.arc and nike.circle, branch on typed errors, read records with verification, reverse an address, and call the API with curl.

Ten minutes from install to a resolved name with its namespace, registry and verification status on screen. Everything below is Arc **testnet** (chain id `5042002`), package `0.1.0-testnet.N`.

<LlmActions />

## Install

```bash
npm install @arcns/resolve@testnet viem
```

`@arcns/resolve` ships on the npm dist-tag `testnet` (pre-releases never move `latest`, so name the tag). As of 2026-09-07 the first testnet release is not on the registry yet: until it lands, clone the repository and `npm ci && npm run build` inside `sdk/`, then `npm link` it. `viem` 2.x is a peer dependency. ESM and CommonJS builds are both shipped. Licence: MIT.

<Callout title="Note" type="info">
  The ArcNS contract addresses are filled into the package when the contracts deployment publishes `deployments/5042002.json`. Until then `createResolver` needs an explicit `deployment` for `@handle`, our `.arc` / `.circle` registry and `reverse()`; the pinned peer registry already answers for `.arc` / `.circle` without one. See [Contracts](/contracts/).
</Callout>

## One resolver, one client

Create one resolver per process, not one per component or request. One instance gives you one shared 5-second cache and one TLD table read.

```ts
// resolver.ts — a module singleton: one viem client, one resolver, one short cache.
import { createPublicClient, http } from "viem";
import { arcTestnet, createResolver, type Resolver } from "@arcns/resolve";

export const arcns: Resolver = createResolver({
  client: createPublicClient({
    chain: arcTestnet,
    transport: http(), // or http(process.env.ARC_RPC_URL)
    // v1 has no CCIP-read for payment records; an OffchainLookup is an error, not data.
    ccipRead: false,
  }),
  // Until the package ships the testnet addresses, or on a fork:
  // deployment: deploymentFromJson(JSON.parse(readFileSync("deployments/5042002.json", "utf8"))),
});
```

The full configuration surface:

```ts
interface ResolverConfig {
  client: PublicClient;                 // viem; chain set; pass ccipRead: false
  deployment?: ArcnsDeployment;         // default ARCNS_DEPLOYMENTS[chainId]; absent: not-configured for our paths
  tldTable?: TldTable;                  // default: loadTldTable(client, deployment.tldDirectory), lazily
  registries?: readonly RegistryPin[];  // extra pins; unpinned refused unless allowUnpinnedRegistries
  allowUnpinnedRegistries?: boolean;
  cacheTtlMs?: number;                  // default 5_000; 0 disables; resolve() only
  now?: () => number;                   // unix seconds for TLD gates
}
```

## Resolve a name

```ts
const r = await arcns.resolve("@nike");
// { name: "@nike", namespace: "handle", registry: "0x…", product: "arcns",
//   address: "0x…", coinType: 2152525650, verification: "verified", epoch: 3n, … }

await arcns.resolve("nike.arc");      // every pinned .arc registry consulted, one labelled answer
await arcns.resolve("nike.circle");   // same, for .circle
await arcns.resolve("@nike.arc");     // throws ResolveError { code: "ambiguous" }
```

Dispatch is on **shape** (`@` prefix, or a known TLD suffix), never "try one, fall back to the other". Use `looksLikeName(input)` to decide whether to attempt resolution at all, so a pasted `0x…` address does not surface a validation error.

Every `Resolved` carries `input`, `name`, `namespace`, `chainId`, `registry`, `product`, `resolver`, `node`, `controller`, `epoch`, `coinType`, `chain`, `address`, `addressBytes`, `verification` and, for TLD names, `tld: { status, sunsetAt }`.

* `namespace` is `handle`, `arc` or `circle`. `namespaceLabel(namespace)` gives the display form (`@handle`, `.arc`, `.circle`).
* `registry` is the contract the answer came from; `product` says whose it is (`arcns` or `khenzarr-arcns`).
* `controller` is who owns the name. It is **never** the payment address and the SDK never falls back to it.
* `epoch` bumps on every ownership change; records from an older epoch are unreachable by construction.
* `verification` is `verified` or `unverified`. See [Verified records](/verified/).

Multi-chain: the default `chain` is `ARC` (ENSIP-11 coin type `0x80000000 | 5042002` = `2152525650`). Pass `{ chain: "ETH" | "BTC" | "SOL" | "X1" }` to read that coin type's record, or `{ chain: "ARC", fallbackChains: ["ETH"] }` to try the coin-type-60 record next; the result names the `coinType` that answered. A name with no record for the requested chain is an explicit `no-record-for-chain`, never a wrong address on the wrong chain.

## Branch on the error code

`resolve()` throws a `ResolveError` with a typed `code`, so a recipient field can tell "still typing" from "does not exist" from "no address for this chain" from "two registries disagree".

```ts
import { ResolveError, namespaceLabel } from "@arcns/resolve";

export async function resolveRecipient(input: string, chain: "ARC" | "ETH" | "BTC" | "SOL" = "ARC") {
  try {
    const r = await arcns.resolve(input, { chain });
    // SHOW ALL OF THESE before enabling "Send":
    //   name + namespaceLabel(r.namespace)      -> "alice.arc  ·  .arc"
    //   short(r.registry) + r.product            -> "registry 0xc3…3a01 (arcns)"
    //   r.verification                           -> "verified" | "unverified" (warn)
    //   r.tld?.status === "sunset"               -> banner: this TLD sunsets on r.tld.sunsetAt
    return r;
  } catch (e) {
    if (!(e instanceof ResolveError)) throw e;
    switch (e.code) {
      case "unrecognized":        return null;                                   // probably a raw address — pass it through
      case "invalid-handle":
      case "invalid-domain":      return null;                                   // still typing; e.reason names the rule
      case "ambiguous":           throw new Error("@name and name.arc are different names — type one of them");
      case "not-found":           throw new Error(`${input} is not registered`);
      case "no-record-for-chain": throw new Error(`${input} has no ${chain} address`);
      case "conflict":            return chooseRegistry(e.candidates!);          // never auto-pick
      case "tld-retired":         throw new Error(`${input}: this TLD has been retired`);
      case "offchain-lookup":
      case "rpc-error":           throw new Error("Couldn't read the chain — try again");
      default:                    throw e;
    }
  }
}
```

| `code`                                    | Meaning                                                                                            |
| ----------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `unrecognized`                            | not a handle or a known domain; probably a raw address                                             |
| `ambiguous`                               | both shapes at once, e.g. `@jack.arc`                                                              |
| `invalid-handle`, `invalid-domain`        | right shape, invalid content; `reason` names the rule (`HyphenAtEdge`, `NonAscii`, `AllDigits`, …) |
| `not-found`                               | valid name, not registered (or `reason: "nft-burned"`); only after a successful chain read         |
| `no-record-for-chain`                     | registered, but no address record for the requested chain; never the controller                    |
| `conflict`                                | pinned registries disagree; `candidates` carries every answer                                      |
| `tld-retired`, `tld-sunset`, `tld-paused` | TLD lifecycle gates (reads: `tld-retired`; writes: all three)                                      |
| `unknown-registry`                        | an unpinned registry address without opt-in                                                        |
| `offchain-lookup`                         | the resolver asked for CCIP-read; v1 treats that as an error, never as data                        |
| `rpc-error`                               | transport or decoding failure; never reported as `not-found`                                       |
| `not-configured`                          | no deployment for this chain; never a guessed address                                              |

## Records with verification per coin type

`records(input)` returns every non-empty address record of a name in our registry, each with its own verification flag.

```ts
const recs = await arcns.records("alice.arc");
for (const rec of recs) {
  // rec = { coinType, chain, value, address, verification }
  //   coinType 2152525650 · chain "ARC" · verification "verified"
  //   coinType 60         · chain "ETH" · verification "verified"
  //   coinType 0          · chain "BTC" · verification "unverified"   -> warn
  //   coinType 501        · chain "SOL" · verification "unverified"   -> warn
}
```

`verification === "verified"` means the owner proved control of that address for that coin type on chain. Anything else is a claimed-but-unproven record: show a warning and make the user look twice. Coin types: `BTC` 0, `ETH` 60, `SOL` 501, `ARC` 2152525650. `text(input, key)` reads an ENS text record from our resolver, or `null`.

## Reverse: the one primary name

```ts
const p = await arcns.reverse("0xce42…54AC");
// { name: "alice.arc", namespace: "arc", registry, product, chainId, node, address, controller } | null
```

There is exactly one primary per address across all three namespaces. The SDK returns it **only if it forward-confirms**: an `@handle` primary must still be owned by the address; a `.arc` / `.circle` primary must resolve back to the address (`addr(node, 0x804cef52)`, then coin type 60) in our registry. A stale string is `null`, never the previous owner. `reverse()` is not cached and makes 2 to 4 `eth_call`s; coalesce concurrent reads of the same address in a table.

## Two registries, one name: `conflict`

Another project sells `.arc` / `.circle` names on Arc testnet from its own registry (`0xc20B…fD1A`, pinned in the SDK as `KHENZARR_ARCNS_TESTNET`). A `.arc` or `.circle` lookup consults **every pinned registry for that TLD** and labels each answer with its `registry` and `product`. Most of the time only one registry holds the name, or both agree, and `resolve()` returns one labelled answer. When they disagree:

```ts
try { await arcns.resolve("alice.arc"); }
catch (e) {
  if (e instanceof ResolveError && e.code === "conflict") {
    for (const c of e.candidates!) {
      // c.product: "arcns" | "khenzarr-arcns"; c.registry: the contract; c.address; c.verification
    }
  }
}
```

Render every candidate with its `product` and `registry` short-address and let the user choose. **Never auto-select.** `resolveAll()` returns the same list without throwing, for search results and "show all matches" surfaces. The API maps `conflict` to `409` with every candidate in the body. A registry address that is not pinned is refused unless you pass `allowUnpinnedRegistries: true`.

## Try the API

`https://api.arcns.io` reads the same chain and returns the same fields for callers that cannot run an EVM client. `@` must be URL-encoded as `%40` in a path.

```bash
curl -s https://api.arcns.io/health
# {"ok":true,"service":"api","version":"…"}   — no RPC call; liveness only

curl -s https://api.arcns.io/resolve/%40nike           # @nike
curl -s https://api.arcns.io/resolve/nike.arc          # every pinned .arc registry consulted
curl -s https://api.arcns.io/resolve/nike.circle
curl -si https://api.arcns.io/resolve/%40nike.arc | head -1   # HTTP 409 — mixed shape, decided with no chain read
curl -s https://api.arcns.io/reverse/0xce42…54AC       # the one primary, forward-confirmed
curl -s https://api.arcns.io/version                   # build, chain id, RPC host, head block
```

The full status contract and every response field are on the [API](/api/) page.

## Next

* Building a send flow: [Wallets](/wallet/).
* Showing names next to addresses: [Explorers](/explorer/).
* What `verified` means and how proofs are made: [Verified records](/verified/).

## Questions

Questions: [support@arcns.io](mailto:support@arcns.io).
