Skip to content
Arc Testnet — names have no monetary value yet
ArcNS / Quickstart

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.2.0-testnet.1.

Install

terminal
# @arcns/resolve is not on the npm registry yet, and its source repository
# is private to FortiBlox Labs — there is no public install path today.
# Read the same data with no install at all, over the public HTTP API:
curl -s https://api.arcns.io/version

@arcns/resolve is 0.2.0-testnet.1 in the (private) sdk/ directory of this repository. It will ship on the npm dist-tag testnet (pre-releases never move latest, so name the tag once it's out: npm install @arcns/resolve@testnet); as of 2026-09-14 it has not been pushed to the registry, and the repository isn't public, so there is nothing an outside developer can npm install or git clone yet. Everything below — resolve(), records(), reverse() and the rest — is the real, currently-shipping SDK surface, generated straight from its TypeScript source, so treat it as a preview of the exact shape you'll get once the package publishes; for integrating today, use the HTTP API instead, which needs no package at all. viem 2.x is a peer dependency of the SDK. ESM and CommonJS builds are both shipped. Licence: MIT.

Note

The Arc testnet contract addresses ship inside the package as ARCNS_DEPLOYMENTS[5042002], mirrored from contracts/deployments/5042002.json (see Contracts), so createResolver({ client }) needs no deployment on Arc testnet. Pass one explicitly only for a fork or a local chain.

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.

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, 
  }),
  // On a fork or a local chain, point it at your own deployment file:
  // deployment: deploymentFromJson(JSON.parse(readFileSync("deployments/5042002.json", "utf8"))),
});

The full configuration surface, from the SDK's own ResolverConfig (sdk/src/resolve.ts):

Prop

Type

Resolve a name

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");      // read from the single ArcNS registry, 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. parseName(input) is that shape decision on its own, with no chain read — a ParsedName:

Prop

Type

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 — always arcns: resolution is single-registry and authoritative (CEO decision 2026-09-09), never a comparison against another registry.
  • 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.

The whole Resolved shape, from sdk/src/types.ts:

Prop

Type

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

resolve-recipient.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 "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;
    }
  }
}
codeMeaning
unrecognizednot a handle or a known domain; probably a raw address
ambiguousboth shapes at once, e.g. @jack.arc
invalid-handle, invalid-domainright shape, invalid content; reason names the rule (HyphenAtEdge, NonAscii, AllDigits, …)
not-foundvalid name, not registered (or reason: "nft-burned"); only after a successful chain read
no-record-for-chainregistered, but no address record for the requested chain; never the controller
tld-retired, tld-sunset, tld-pausedTLD lifecycle gates (reads: tld-retired; writes: all three)
offchain-lookupthe resolver asked for CCIP-read; v1 treats that as an error, never as data
rpc-errortransport or decoding failure; never reported as not-found
not-configuredno 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.

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.

Each entry is an AddressRecord (sdk/src/records.ts):

Prop

Type

Reverse: the one primary name

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

The answer is a Primary (sdk/src/resolve.ts):

Prop

Type

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 3 eth_calls for a resolved @handle primary, 4–5 for a resolved .arc / .circle primary (fewer on an early exit, e.g. no reverse record set at all); coalesce concurrent reads of the same address in a table.

Single-registry, authoritative

.arc / .circle resolve only through the FortiBlox ArcNS registry (deployment.ensRegistry). There is no cross-resolution against, comparison with, or warning about any other registry on Arc — resolve() / resolveAll() never read a second registry, and a name this registry does not hold is a plain ResolveError { code: "not-found" }, exactly like any other unregistered name (CEO decision 2026-09-09: single-registry, winner-take-all).

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.

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

curl -si https://api.arcns.io/resolve/%40circle | head -1
# HTTP/2 402 — from a terminal you are not a first-party browser: read the
# PAYMENT-REQUIRED header and pay $0.001 over x402, or present an X-API-Key.

curl -s https://api.arcns.io/quote/%40alice     # x402 as well — price, availability, commit window
curl -s https://api.arcns.io/version            # free: build, chain id, RPC host, head block

Reads are free for browsers on the ArcNS sites and for API-key holders, and sold to everyone else at $0.001 per answer over x402 v2 — the SDK, which reads the chain directly, is always free. The full status contract, every response field and the payment flow are on the API page.

Next

Questions

Questions: [email protected].

Browse the docs