AI agents & integrators
.ether is a good home for machine-owned names because there is nothing in the
way: no owner, no admin keys, no API keys, no accounts, no rate limits, no
company that can gate, reprice or revoke a registration. Every function below
is a public method on an immutable contract. Authorization is plain
msg.sender: there are no tx.origin checks and no EOA-only gates, so a smart
account (ERC-4337), a delegated EOA (EIP-7702), an MPC wallet and a plain hot
key all work identically.
Contract addresses and the full function reference live on the
contracts page, and the source is published there and on the
audit trail. This protocol runs only on Ethereum mainnet, chain id
1. Verify both the address and chainid == 1 before signing: the same
address on another chain is not this contract.
Five rules that prevent lost funds
Read these before writing any registration code. Each one is a real failure mode.
-
Persist a strong secret before you commit. Registration is commit–reveal: the reveal needs the same
(label, owner, secret)you committed. The secret must be 32 bytes from a CSPRNG, unique per commitment, never derived from the label, time or address; the commitment is a salted hash and its only hiding property is that entropy. A guessable secret lets an observer reconstruct your commitment, learn the label, and race you. Write the secret to durable storage before broadcastingcommit, or a crash leaves you with a commitment you can never reveal. -
Reveal from the account you committed for.
reveal(label, secret)recomputes the commitment withmsg.senderand mints tomsg.sender. If you commit for address A and reveal from address B, it revertsCommitmentNotFound. Theownerargument ofmakeCommitment(label, owner, secret)must be the account that later callsreveal, or the target of arevealFor. -
A reverted reveal keeps the commitment; only re-committing needs a fresh secret. If a reveal fails, retry the same reveal with the same secret: the commitment survives (the revert is atomic). A commitment stays live for 24 hours, and re-submitting the identical commitment while it is live reverts
AlreadyCommitted. So you only need a new secret when starting over fromcommit, not when retrying a reveal. Before any retry, checkownerOf: a front-runner may already have registered the name to you (see revealFor). -
Names freeze the second they expire. The moment
block.timestamppassesexpiresAt, the name stops resolving (resolvereturns the zero address), and transfers and record writes revert, through the entire 90-day grace period. Onlyrenewworks. Gate any “is this name usable” logic onresolve(tokenId) != address(0), not onisExpired/inGracePeriod; those two describe only top-level names and always returnfalsefor subdomains, whose liveness follows their parent chain. -
Token ids are reused across epochs. If a name lapses past grace and is re-registered, the same
tokenIdburns and re-mints to the new owner, and all resolver records are wiped. If you cache anything keyed on a tokenId, storerecords(tokenId).epochnext to it and treat a changed epoch as a different name owned by a stranger.
Registering a name, step by step
1. label = isAscii(name) ? lowercase(name) : ENSIP-15 normalize(name)
2. require isAvailable(label, 0)
3. quote = getFee(bytes(label).length) + getPremium(computeId(label + ".ether"))
4. secret = 32 CSPRNG bytes, unique (persist it now)
5. send commit(makeCommitment(label, owner, secret))
6. wait until reveal block timestamp >= commit block timestamp + 60
(commitment stays valid 24 h from the commit; both boundaries inclusive)
7. send reveal{value: quote}(label, secret) (from `owner`)
8. confirm ownerOf(tokenId) == owner
Details that matter to a program:
- Normalization. The contract lowercases ASCII and accepts a permissive
charset (most printable ASCII except space, control characters and dot). It
does not understand Unicode. Apply
ENSIP-15 normalization only to non-ASCII
labels; do not run it on plain ASCII, because ENSIP-15 remaps and rejects
ASCII that the contract accepts (it maps a straight apostrophe to U+2019 and
rejects interior underscores), which would register a different name than the
user typed. The contract exposes
isAsciiLabel(label). Fees are tiered by the label’s UTF-8 byte length (see pricing). - Timing. The reveal window is
commit + 60 stocommit + 24 h, boundaries inclusive, measured against block timestamps. A scheduler tick of 61–70 seconds is plenty. Revealing one block too early revertsCommitmentTooNew(harmless, retry). - Value and refunds. Send the quoted
fee + premium; anything above the amount due at execution time is refunded tomsg.senderin the same transaction. Once a name is registrable, the premium only decays, so a quote taken then keeps sufficing. Two boundary cautions: a name still in grace quotes a premium of 0 while being unregisterable, then the premium jumps to ~100 ETH at the first registrable second, so if you pre-commit to snipe an expiring name, budgetgetPremiumas of the reveal block. And if your sender is a contract, it must accept ETH (receive()) or pay exactly the amount due, or the refund reverts the whole registration. - Safe-mint. Every mint runs an
onERC721Receivedcheck. Any contract that is to receive a name (therevealsender itself, arevealForowner, or a subdomainto) must implementonERC721Receivedor the transaction reverts. - Races. If someone registers the label first, your reveal reverts
AlreadyRegisteredand your ETH never leaves. The commit hash reveals nothing about the label, and the commitment binds the owner, so observing your transactions gives an attacker no way to take the name from you. - Failure is atomic. Every revert path returns the full
msg.valueand costs only gas, never the fee. OnCommitmentTooOld(window closed) orCommitmentNotFound(already consumed), stop retrying: start over with a fresh secret, and checkownerOffirst.
Sponsored registration: revealFor
revealFor(label, owner, secret) is reveal with the owner made explicit: the
commitment must bind owner, the name mints to owner, and the caller pays and
receives any refund. This is how a funder provisions a name directly into an
agent’s wallet that holds no ETH, with no custody hop and no transfer step.
sponsor: commit(makeCommitment(label, agentWallet, secret))
sponsor: wait >= 60 s (block timestamps)
sponsor: revealFor{value: quote}(label, agentWallet, secret)
result: ownerOf == agentWallet, sponsor paid, sponsor got the refund
Safety properties, verified by symbolic proof and the test suite:
- The commitment binds the recipient, so a mempool observer cannot redirect the
mint. The only thing a front-runner can do is pay for your registration: the
name still lands on the committed owner. If your own transaction then reverts
CommitmentNotFound, checkownerOfbefore retrying; you may already own the name. - The recipient never receives ETH and cannot grief the payer beyond making the transaction revert, which refunds it.
Two footguns are yours to avoid. A contract recipient must implement
onERC721Received or the transaction reverts. And a typoed owner is
unrecoverable: it mints the name (and burns the fee) to a stranger with no
admin to appeal to. Checksum-validate the recipient and simulate before
broadcasting.
Renewal
renew(tokenId) is deliberately permissionless: anyone can pay to renew any
top-level name. Renewal always extends 365 days from the current expiry, so you
can prepay several years in one batched transaction, and a keeper, a sponsor or
the agent itself can keep a name alive without owning it. renew refunds excess
to msg.sender, so when batching, send each call exactly its fee: inside a
multicall, msg.sender is the multicall contract, not you, and an overpay
either reverts the batch or strands the refund. Renewal works through the 90-day
grace period; past grace the name is gone and must be re-registered (a decaying
premium applies, see pricing). Monitor expiresAt(tokenId) and
renew with margin: an agent whose identity records freeze mid-task is a worse
failure than a week of extra prepayment.
Identity records for agents
A .ether name gives an agent a resolvable, owner-controlled identity with no extra contracts:
- Address.
setAddrpoints the name at the agent’s operating wallet;resolvefalls back to the owner until it is set. - Primary name.
setPrimaryNameis callable by the owner or by the address the name currently resolves to. That enables a clean split: a cold wallet owns the name and points it at the agent’s hot wallet, and the hot wallet claims the reverse record itself, with no key sharing and no operator approvals. - Agent card / metadata.
setContenthashcan point at an IPFS CID holding the agent’s card, for example an ERC-8004 registration file (ERC-8004, still a draft, allowsipfs://registration URIs, which a .ether name serves natively). An A2A agent card is canonically discovered at a well-known HTTPS URL, so treat an IPFS copy as a mirror, not the primary. For HTTP-hosted metadata use text records with standard keys (url,avatar,description). - ERC-8004 binding. If you register the agent in an
ERC-8004 identity registry, publish
the binding with the ENSIP-25
text-record key format verbatim
(
agent-registration[<registry>][<agentId>]= any non-empty value). Use the upstream format exactly rather than inventing a variant. Deployed ENSIP-25 verifiers today resolve through the ENS registry, so treat this as forward-compatible convention, not live interop.
Records are wiped on re-registration (rule 5), which is a feature for identity: a lapsed agent’s endpoints can never silently point a stranger’s name at your infrastructure, or vice versa.
Token-bound accounts: the hazard first
ERC-6551 gives every ERC-721 a deterministic token-bound account, and it works on .ether names with no contract support. Understand the failure mode before putting anything of value in one: if the name lapses past grace, re-registration re-mints the same tokenId to the new registrant, who then controls the old token-bound account and everything in it, for as little as the base fee. Ownership of the name is ownership of the account, across epochs. Do not treat a name-bound account as a treasury unless renewal is automated with wide margin; prefer holding assets in the agent’s own wallet and using the name purely as identity.
Subdomain fleets
For fleets of agents, subdomains are simpler than top-level names: no commit–reveal, one transaction, free under a parent you own.
registerSubdomainFor(label, parentId, to)mintsworker.fleet.etherdirectly to any wallet. The parent owner stays in control.- To buy subdomains from the SubdomainRegistrar,
read
config(parentId)for the price and approve exactly that amount (never an unlimited allowance) or send exactly that ETH. The parent controller can change the price at any time and your purchase pays the price at execution; an exact approval or exact value caps your exposure, since a price raise then simply reverts your transaction. An agent holding only stablecoins can buy an ERC-20-priced subdomain withapprove+register, no ETH beyond gas, and the mint is atomic with payment. - The trust model is explicit: a parent owner can always reclaim a subdomain by re-registering its label. A subdomain is trust in the parent, right for an operator’s own fleet, priced accordingly when buying from strangers.
Privacy note
Availability and quote lookups leak intent. isAvailable, getFee and
getPremium need no API key, but their call arguments contain the label (or its
namehash, which is unsalted and reversible by dictionary), so asking a
third-party API or a public RPC “is alice free?” reveals the label to that
operator before anything binds it to you. Run those view calls against your own
node or a provider you trust. The commit itself reveals nothing (it is a salted
hash), so the label first appears on-chain at reveal time, when the commitment
already binds it to you.
Quick reference
| Fact | Value |
|---|---|
| Chain | Ethereum mainnet (chain id 1) |
| Commitment window | commit + 60 s to commit + 24 h (inclusive) |
| Registration / grace | 365 days / 90 days |
| Fees (1/2/3/4/5+ bytes) | 0.05 / 0.05 / 0.005 / 0.005 / 0.0005 ETH, burned |
| Post-grace premium | starts near 100 ETH, linear to 0 over 21 days |
| Reveal binds | msg.sender (reveal) or explicit owner (revealFor) |
| Activity check | resolve(tokenId) != address(0) |
| Re-registration | same tokenId, epoch bumps, records wiped |
A machine-readable summary of this page lives at
/llms.txt. Function signatures, events and addresses are on the
contracts page.