Integrate identity
A .ether name is a portable onchain identity your app can read without asking
anyone. No API keys, no accounts, no rate limits, no company in the middle: a
name resolves to an address, carries public records (avatar, links, bio), and
can hand out subdomains that act as memberships. Everything on this page is a
view call on an immutable contract, so reading an identity costs nothing and
can never be revoked or repriced under you.
This is the read side: consuming identities other people own. To register names programmatically, see AI agents & integrators; for the full ABI and events, see the Contracts reference.
The model in one minute
- Every name is an ERC-721 token on the NameNFT contract, which is also the resolver: one contract holds ownership, addresses and records together.
- A name’s
tokenIdis the uint256 of its namehash, rooted at the.ethernode. Soalice.ether, its records, and its subdomains all hang off one deterministic id. - Given that id, everything is a view call:
resolvefor the address,textfor records,ownerOffor the holder,recordsfor lifecycle state. - The protocol runs only on Ethereum mainnet (chain id 1). The same address
on any other chain is not this contract; confirm
chainId === 1before you trust a read.
Setup
The examples use viem. The same calls work with ethers,
wagmi, or a raw eth_call.
import { createPublicClient, http, namehash, parseAbi } from 'viem'
import { normalize } from 'viem/ens'
import { mainnet } from 'viem/chains'
// NameNFT: registry, resolver and ERC-721 in one. Address on the Contracts page.
const NAME_NFT = '0x…' as const
const abi = parseAbi([
'function resolve(uint256 tokenId) view returns (address)',
'function reverseResolve(address addr) view returns (string)',
'function text(uint256 tokenId, string key) view returns (string)',
'function ownerOf(uint256 tokenId) view returns (address)',
'function isExpired(uint256 tokenId) view returns (bool)',
'function records(uint256 tokenId) view returns (string label, uint256 parent, uint64 expiresAt, uint64 epoch, uint64 parentEpoch)',
])
const client = createPublicClient({ chain: mainnet, transport: http() })
const read = (functionName: string, args: unknown[]) =>
client.readContract({ address: NAME_NFT, abi, functionName, args })
// A name's tokenId is the uint256 of its namehash.
const nameToId = (name: string) => BigInt(namehash(normalize(name)))
nameToIdcomputes the id in the browser, which is fine for display..etheruses standard namehash, but for anything security-sensitive let the contract be the source of truth: it exposes purecomputeNamehash(name)andnormalize(label)helpers that always match how it stored the name.
Resolve a name to an address
The everyday lookup: turn alice.ether into the address it points at.
const address = await read('resolve', [nameToId('alice.ether')])
// 0x0000…0000 when the name is unset, expired, or does not exist.
resolve falls back to the name’s owner when no address record is set, so a
freshly registered name resolves to its owner with zero setup. Treat the zero
address as “no resolution” and handle it.
Resolve an address to a name
The reverse: show alice.ether instead of 0xd8dA…6045.
const name = await read('reverseResolve', [user]) // '' when none is set
This returns the address’s primary name. The contract only honors it while that name is active and still resolves back to the same address, so a stale or sold name can never be used to impersonate someone. Even so, treat it as a display convenience: for authorization, verify ownership (below) rather than trusting a label.
Build a profile from records
Names carry free-form text records under keys most wallets already understand. Read several in one round trip with multicall:
const id = nameToId('alice.ether')
const keys = ['avatar', 'url', 'description', 'com.twitter', 'com.github', 'org.telegram']
const values = await client.multicall({
contracts: keys.map((key) => ({ address: NAME_NFT, abi, functionName: 'text', args: [id, key] })),
allowFailure: false,
})
const profile = Object.fromEntries(keys.map((key, i) => [key, values[i]]))
| Key | Meaning |
|---|---|
avatar |
Image, usually an ipfs://… URI |
url |
Website |
description |
Short bio |
com.twitter / com.github / org.telegram |
Social handles |
For the avatar, fall back to the name’s onchain certificate (tokenURI) when
the record is empty: every name has a deterministic card drawn onchain, so there
is always something to show. You can also read any custom key your own app
writes, covered under Store your app’s data.
Gate access by ownership
Token-gating on a specific name, or on holding any name at all:
async function holdsName(name: string, user: `0x${string}`) {
const id = nameToId(name)
try {
const [owner, expired] = await Promise.all([read('ownerOf', [id]), read('isExpired', [id])])
return owner.toLowerCase() === user.toLowerCase() && !expired
} catch {
return false // ownerOf reverts for a name that was never registered
}
}
Ownership alone is not enough: a name can expire and lapse to someone else.
Always pair ownerOf with isExpired (or inGracePeriod) so you never grant
access on a dead name, and re-check at the moment of access rather than caching
the result forever.
Gate a community by subdomain
The membership pattern: a community owns dabot.ether and hands out subdomains
(alice.dabot.ether) as passes. An event, a forum, or a chat gate then admits
only holders of a live subdomain under that parent.
Verifying a member’s claimed name takes three checks: they hold it, it is really a child of your parent, and the parent has not changed hands since it was minted.
const PARENT = nameToId('dabot.ether')
async function isMember(claimed: string, user: `0x${string}`) {
const id = nameToId(claimed) // e.g. 'alice.dabot.ether'
try {
const [owner, sub, parent, parentExpired] = await Promise.all([
read('ownerOf', [id]),
read('records', [id]),
read('records', [PARENT]),
read('isExpired', [PARENT]),
])
return (
owner.toLowerCase() === user.toLowerCase() && // holds the subdomain
sub.parent === PARENT && // it is a child of dabot.ether
sub.parentEpoch === parent.epoch && // parent has not been re-registered since
!parentExpired // and the community is still live
)
} catch {
return false
}
}
The parentEpoch === parent.epoch check is the important one. Subdomains do not
expire on their own, but if the parent ever lapses past its grace period and is
re-registered, the contract bumps the parent’s epoch and invalidates every
subdomain from the previous era. Skip this check and you keep admitting
members whose passes a new owner has already wiped. To list every current member
instead of verifying one, index the SubdomainRegistered(tokenId, parentId, label) event for your parentId and apply the same liveness checks.
Attributes and verification
Text records are self-asserted: the owner can write anything. That is perfect for a bio or a link, and not enough on its own for a claim like age or KYC.
For trustless attributes, store an attestation rather than a bare value: have a trusted attester sign a statement (for example, that a given address is over 18, with an expiry), publish it in a record under your namespace, and have your app verify the attester’s signature before it trusts the claim. The record is only transport; the signature is what you check. Apply the same expiry and epoch discipline as everywhere else.
Store your app’s data
Your app can also write to a name whose owner has connected, keeping its own namespace on every name:
// Owner-only, sent from the owner's wallet client. Reverse-DNS keys stay collision-free.
await walletClient.writeContract({
address: NAME_NFT, abi, functionName: 'setText',
args: [nameToId('alice.ether'), 'com.myapp.profile', value],
})
Use a reverse-DNS key you control (com.myapp.*) and no other app can clash with
you. Records are wiped automatically when a name expires and is re-registered, so
a new owner never inherits a stranger’s data. For anything bigger than a string,
deploy your own contract keyed on the tokenId and store the name’s epoch
alongside your data, treating a changed epoch as a fresh identity. Both patterns
are covered in Building on EtherNames.
Rules that keep integrations correct
- Mainnet only. Everything is on Ethereum mainnet, chain id 1. Verify the chain and the contract address before trusting any read.
- Zero address means no resolution.
resolvereturns0x000…0for unset, expired, or nonexistent names. Handle it explicitly. - Expiry and grace are real. A name can lapse and change hands. Pair
ownership with
isExpired/inGracePeriod, and re-check rather than caching forever. - Token ids are reused. A name that lapsed and was re-registered is a
different identity with the same id. For anything you persist, store
records(id).epochand treat a change as a new owner. - Beware look-alikes. Confusable and homograph labels can be used to deceive; normalize before comparing, and never rely on a label alone for anything that matters, verify ownership onchain.
- The chain is the source of truth. Never trust a frontend’s claim of ownership or resolution; confirm it with a view call.
Next steps
- Contracts reference: every function, event and constant.
- Records & resolution: the record types in depth.
- AI agents & integrators: registering and managing names programmatically.