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
# @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.
// 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 }.
namespaceishandle,arcorcircle.namespaceLabel(namespace)gives the display form (@handle,.arc,.circle).registryis the contract the answer came from;productsays whose it is — alwaysarcns: resolution is single-registry and authoritative (CEO decision 2026-09-09), never a comparison against another registry.controlleris who owns the name. It is never the payment address and the SDK never falls back to it.epochbumps on every ownership change; records from an older epoch are unreachable by construction.verificationisverifiedorunverified. 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".
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;
}
}
}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 |
tld-retired, tld-sunset, tld-paused | TLD lifecycle gates (reads: tld-retired; writes: all three) |
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.
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 } | nullThe 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 blockReads 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
- Building a send flow: Wallets.
- Showing names next to addresses: Explorers.
- What
verifiedmeans and how proofs are made: Verified records.
Questions
Questions: [email protected].