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.

  1. 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 broadcasting commit, or a crash leaves you with a commitment you can never reveal.

  2. Reveal from the account you committed for. reveal(label, secret) recomputes the commitment with msg.sender and mints to msg.sender. If you commit for address A and reveal from address B, it reverts CommitmentNotFound. The owner argument of makeCommitment(label, owner, secret) must be the account that later calls reveal, or the target of a revealFor.

  3. 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 from commit, not when retrying a reveal. Before any retry, check ownerOf: a front-runner may already have registered the name to you (see revealFor).

  4. Names freeze the second they expire. The moment block.timestamp passes expiresAt, the name stops resolving (resolve returns the zero address), and transfers and record writes revert, through the entire 90-day grace period. Only renew works. Gate any “is this name usable” logic on resolve(tokenId) != address(0), not on isExpired/inGracePeriod; those two describe only top-level names and always return false for subdomains, whose liveness follows their parent chain.

  5. Token ids are reused across epochs. If a name lapses past grace and is re-registered, the same tokenId burns and re-mints to the new owner, and all resolver records are wiped. If you cache anything keyed on a tokenId, store records(tokenId).epoch next 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 s to commit + 24 h, boundaries inclusive, measured against block timestamps. A scheduler tick of 61–70 seconds is plenty. Revealing one block too early reverts CommitmentTooNew (harmless, retry).
  • Value and refunds. Send the quoted fee + premium; anything above the amount due at execution time is refunded to msg.sender in 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, budget getPremium as 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 onERC721Received check. Any contract that is to receive a name (the reveal sender itself, a revealFor owner, or a subdomain to) must implement onERC721Received or the transaction reverts.
  • Races. If someone registers the label first, your reveal reverts AlreadyRegistered and 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.value and costs only gas, never the fee. On CommitmentTooOld (window closed) or CommitmentNotFound (already consumed), stop retrying: start over with a fresh secret, and check ownerOf first.

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, check ownerOf before 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. setAddr points the name at the agent’s operating wallet; resolve falls back to the owner until it is set.
  • Primary name. setPrimaryName is 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. setContenthash can point at an IPFS CID holding the agent’s card, for example an ERC-8004 registration file (ERC-8004, still a draft, allows ipfs:// 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) mints worker.fleet.ether directly 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 with approve + 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.