v2 is live: 70,384 ENS names migrated to permanent @eth names→
[N.01/02]> Documentation

Everything you need to understand and integrate Stele, without unnecessary complexity.
[N.02/02]> Quick start

Stele is a single smart contract with a simple mapping. Check the docs for the ABI and integration examples.

  • Resolve names in any app with one read call: viem, ethers or any JSON-RPC client. Unregistered names return the zero address.

Stele/ resolve.ts
1import { createPublicClient, http, parseAbi } from "viem";2import { mainnet } from "viem/chains";34const client = createPublicClient({ chain: mainnet, transport: http() });5const registry = parseAbi([6  "function getAddress(string) view returns (address)",7  "function getName(address) view returns (string)",8]);910// Name → address (returns 0x0 when unregistered)11const address = await client.readContract({12  address: REGISTRY, abi: registry,13  functionName: "getAddress", args: ["alice@eth"],14});1516// Address → name (lets a wallet verify the recipient)17const name = await client.readContract({18  address: REGISTRY, abi: registry,19  functionName: "getName", args: [address],20});

Stele is an immutable address book for Ethereum. These docs cover the name format, namespaces, fees, registration and resolution, and how to name smart contracts. For the full contract API (functions, events, types), see the API reference on GitHub.

Overview

Stele is an immutable address book for Ethereum. It maps human-readable names to Ethereum addresses so you can send crypto to vitalik@eth instead of 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045. With Stele, applications and wallets can show user-friendly names in place of long hexadecimal addresses, making every Ethereum interaction simpler and safer.

Key properties:

  • Permanent: Each name is irrevocably bound to an Ethereum address; no expiration, no transfer, no resale.
  • Fully on-chain: No off-chain dependencies. Names and addresses can be resolved entirely on-chain via getName and getAddress function.
  • Universal naming: Both EOAs and smart contracts can be named in the same unified registry.
  • Globally unique: Each name is unique across all Ethereum addresses, preventing conflicts like duplicate ERC20 token names.
  • Permissionless namespaces: Anyone can create their own namespace without requiring approval from any central party.
  • Private namespaces: Supports private namespaces where the namespace owner maintains exclusive control over name registrations within their namespace.
  • ETH burning: 80% of registration fees are permanently burned, supporting Ethereum's deflationary mechanism.

Name Format

Stele names follow the format <label>@<namespace>.

Labels and namespaces are subject to the following format rules:

  • Must be 1–20 characters long
  • Must consist only of lowercase letters (a-z), digits (0-9), and hyphens (-)
  • Cannot start or end with -
  • Cannot contain consecutive hyphens (--)

Valid name examples:

  • ✅ alice@eth
  • ✅ vitalik@100x
  • ✅ crypto-degen@yolo
  • ✅ to-the-moon@bull
  • ✅ gm@wen-lambo
  • ✅ 2-rich@4-real

Invalid name examples:

  • ❌ thisisaveryverylongname@eth (label too long)
  • ❌ Name@eth (uppercase in label)
  • ❌ gm$web3@xyz (special character in label)
  • ❌ -name@gm (label cannot start with hyphen)
  • ❌ name-@og (label cannot end with hyphen)
  • ❌ my--name@888 (label cannot have consecutive hyphens)

The same format rules apply to namespaces. For example, --name, $rich, and -ns are all invalid and cannot be registered.

Namespaces

Stele features two types of namespaces: public and private.

Public Namespaces:

  • Anyone can register names within a public namespace after a 7-day exclusivity period after namespace creation has ended.
  • During the exclusivity period, only the namespace owner can register or sponsor names.
  • Namespace owners receive 10% of all name registration fees in perpetuity.
  • Registration fee: 50 ETH.

Private Namespaces:

  • Only the namespace owner can register names forever.
  • Namespace owners do not receive fees; all fees go to the Stele contract owner.
  • Registration fee: 10 ETH.

Anyone can register a new namespace by paying the one-time registration fee.

ETH Burn and Fee Distribution

  • 80% of ETH sent is burnt via DETH.
  • 20% is credited as fees:
    • Public namespaces: 10% to namespace owner, 10% to Stele contract owner
    • Private namespaces: 20% to Stele contract owner

Notes:

  • Namespace owners only receive fees from name registrations in their namespace (public namespaces only).
  • All ETH burns are recorded via the DETH contract, a global ETH sink and burn attestation registry. Burns are tracked and verifiable as non-transferrable DETH credits minted at a 1:1 ratio, providing proof of contribution to Ethereum's deflationary mechanism.

Compared with ENS

ENS replicates the web2 domain model with a rent-seeking approach:

  • Names expire: Users must renew their names, creating ongoing subscription costs for users.
  • Name sniping: If a user forgets to renew, others can grab their name, especially if it previously received funds. If they don't notice and continue sharing the name, funds will be lost.
  • Transferable names: ENS names are transferable and tradeable, thereby encouraging speculation rather than use as payment identifiers.
  • Complex: Unnecessarily complex architecture for the purpose of simple name-to-address mapping.
  • Lack of Ethereum alignment: ENS has a token that does not provide direct value accrual to ETH holders.
  • Dot format (name.eth): Visually identical to web2 domains, despite serving a fundamentally different purpose.

Stele takes a fundamentally different approach:

  • Names are permanent and never expire.
  • Names are non-transferable, discouraging speculation.
  • Aligned with ETH holders: 80 % of registration fees (paid in ETH) are burned, accruing value to ETH holders by reducing the supply. No valueless governance token needed.
  • @ format (label@namespace): Clearly distinct from web2 domains, signaling a payment identifier rather than a website.

Beyond these core differences, Stele offers additional capabilities: permissionless namespace registration (for a fee), support for private namespaces with exclusive control as well as smart contract naming.

How It Works

Name Registration

The simplest way to register a name within a public namespace is using the x2xPay payments app: connect your wallet, pick an available name, and pay the one-time registration fee.

To register an EOA name via Etherscan, connect the wallet that should own the name and call registerName with the label, namespace, and fee. Excess ETH is refunded. Wait a few blocks, then verify with getAddress or getName.

Example: Registering bob@eth in the eth namespace (costs 0.001 ETH).

Note: registerName only works for public namespaces after the exclusivity period (7 days) has ended. During the exclusivity period or for private namespaces, namespace owners must use registerNameWithAuthorization, even for their own registrations.

Example scripts:

Smart contracts can be named as well. See Smart Contract Naming.

Name Registration With Authorization

Stele supports authorized name registration via registerNameWithAuthorization, which allows a third party (sponsor) to pay the registration fee and gas costs while the recipient explicitly authorizes the registration via an EIP-712 signature.

How it works:

  1. The recipient signs an EIP-712 message authorizing a specific name registration (label, namespace, recipient address, and validUntil expiry).
  2. The sponsor calls registerNameWithAuthorization with the recipient's signature and pays the registration fee.
  3. The name is registered to the recipient's address.

Use cases:

  • Organizations registering names for their team members.
  • Projects airdropping names to their community.
  • Enabling contract wallets (EIP-1271) to register names, as contracts can't send transactions themselves.
  • Any scenario where someone else pays registration fees on behalf of recipients.

Important restrictions:

  • For public namespaces: During the 7-day exclusivity period, only the namespace owner can sponsor registrations.
  • For private namespaces: Only the namespace owner can sponsor registrations forever.

Batch registration:

  • Stele also supports batchRegisterNameWithAuthorization to register multiple names within the same namespace in a single transaction. See the API documentation for details.

Example scripts:

Name Resolution

Stele provides simple on-chain resolution for names and addresses.

Look up Address from Name: getAddress

  • Resolve a name like vitalik@001 or bob@eth to its Ethereum address.
  • Strings without an @ namespace separator are invalid and return address(0).
  • Works directly on Etherscan or any Ethereum interface.
  • Returns zero address if the name is not registered.

Look up Name from Address: getName

  • Find the Stele name for any Ethereum address.
  • Returns the full name format (e.g., alice@001).
  • Returns an empty string if the address has no name.

Example scripts:

Namespace Registration

How to register a public namespace:

  1. Choose a Namespace: Select an available namespace and set the desired price per name (must be at least 0.001 ETH and a multiple of 0.001 ETH).
  2. Register Namespace: Submit a transaction with the required ETH to register the namespace (see registerPublicNamespace in the API docs). Any excess will be refunded.

Public namespace owners have an exclusive 7-day window to register or sponsor any name within their namespace. After this period, anyone can freely register names via registerName. During the exclusivity period, use registerNameWithAuthorization even for their own registrations.

How to register a private namespace:

  1. Choose a Namespace: Select an available namespace and set the desired price per name (must be at least 0.005 ETH and a multiple of 0.001 ETH).
  2. Register Namespace: Submit a transaction with the required ETH to register the namespace (see registerPrivateNamespace in the API docs). Any excess will be refunded.

The private namespace owner registers names via the authorized flow (see registerNameWithAuthorization in the API docs). This is the only way to register names in private namespaces, including registrations for the namespace owner themselves.

Notes:

Example: Public Namespace Registration via Etherscan

Namespaces can be registered directly via Etherscan.

To register a private namespace, use the registerPrivateNamespace function with the same input fields as the public version. The required registration fee is 10 ETH (entered in the first field instead of 50); if more than 10 ETH is sent, the excess will be automatically refunded.

Example scripts:

Querying Namespace Information

Namespace details can be retrieved using getNamespaceInfo. The details include:

  • Price per name
  • Namespace owner address
  • Creation timestamp
  • Whether it's private or public

The exclusivity period can be checked using isInExclusivityPeriod, which returns true if the namespace is still within the 7-day exclusivity window.

Example scripts:

Claiming Fees

Fees earned by namespace owners and the Stele contract owner accumulate within the Stele contract and must be claimed to be withdrawn. Available actions:

Example scripts:

Price list

Every public namespace has a fixed price per name, whatever the label length. The complete, searchable list lives on the pricing page. Private namespaces are restricted: only the namespace owner can register names in them.

Prices on Sepolia may differ from mainnet. To obtain the current price for any namespace, use the getNamespaceInfo or getNamespacePrice functions.

Smart Contract Naming

Smart contracts can receive a Stele name (e.g., myprotocol@eth) so users can identify them by a human-readable name instead of a long address.

Note: The following examples demonstrate naming for contracts that are only deployed on Ethereum. For contracts deployed on multiple chains, see the Using Stele Names with Multi-Chain Deployments section below for important guidance and considerations.

There are four ways to name a smart contract:

Option 1: Register via Constructor

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {IXNSv2} from "./interfaces/IXNSv2.sol";

contract MyProtocol {
    constructor(address _xns, string memory label, string memory namespace) payable {
        IXNSv2(_xns).registerName{value: msg.value}(label, namespace);
    }

    /// @notice Optional: Accept ETH refunds from XNS if excess payment is sent.
    /// Not needed if the correct price is sent, without any excess.
    receive() external payable {}
}

Deploy with the label, namespace, and required payment to register the name during contract creation.

See MockERC20A and the registerNameForERC20A.ts script for an example of how to register a name for an ERC20 token using the constructor method.

Notes:

  • Any excess payment is refunded by Stele to msg.sender, which will be the contract. Be sure to implement a receive() function to accept ETH payments, and provide a way to withdraw any refunded ETH if needed. To avoid receiving refunds altogether, send exactly the required payment when deploying the contract.
  • The registerName function only works for public namespaces after the exclusivity period (7 days) has ended. For private namespaces, contracts must use Option 4 (EIP-1271).

Option 2: Register via Separate Function

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {IXNSv2} from "./interfaces/IXNSv2.sol";

contract MyProtocol {
    IXNSv2 public immutable xns;

    constructor(address _xns) {
        xns = IXNSv2(_xns);
    }

    /// @notice Register an XNS name for this contract
    /// @param label The label to register (e.g., "myprotocol")
    /// @param namespace The namespace to register in (e.g., "eth")
    function registerName(string calldata label, string calldata namespace) external payable {
        xns.registerName{value: msg.value}(label, namespace);
    }

    /// @notice Optional: Accept ETH refunds from XNS if excess payment is sent.
    /// Not needed if the correct price is sent, without any excess.
    receive() external payable {}
}

Note: Add access control as needed.

After deployment, call registerName("myprotocol", "eth") with the required payment to register the name.

See MockERC20B and the registerNameForERC20B.ts script for an example of how to register a name for an ERC20 token using the separate registerName function approach.

Notes:

  • Any excess payment is refunded by Stele to msg.sender, which will be the contract. Be sure to implement a receive() function to accept ETH payments, and provide a way to withdraw any refunded ETH if needed. To avoid receiving refunds altogether, send exactly the required payment when calling registerName.
  • The registerName function only works for public namespaces after the exclusivity period (7 days) has ended. For private namespaces, contracts must use Option 4 (EIP-1271).

Option 3: Register for Owned Contract

For already-deployed contracts that expose owner() or getOwner() but cannot call registerName and do not implement EIP-1271, the contract owner registers the name from outside the target contract — typically via a Hardhat/ethers script or Etherscan. No changes to the target contract are required.

The owner wallet is msg.sender; the protocol contract address is recipient. Stele checks that recipient.owner() (or getOwner()) equals the owner wallet, then assigns the name to recipient, not to the owner.

import hre from "hardhat";

const xnsAddress = "0x..."; // XNS contract on Ethereum
const myProtocolAddress = "0x..."; // Already-deployed contract to name
const label = "myprotocol";
const namespace = "eth";
const price = hre.ethers.parseEther("0.001");

const owner = (await hre.ethers.getSigners())[0]; // Must equal myProtocol.owner()
const xns = await hre.ethers.getContractAt("XNSv2", xnsAddress);

await xns.connect(owner).registerNameForOwnedContract(
  myProtocolAddress,
  label,
  namespace,
  { value: price },
);
// → "myprotocol@eth" resolves to myProtocolAddress, not owner.address

Ownership is verified exclusively against the contract deployed at recipient on Ethereum.

See registerNameForOwnedContract.ts for a runnable script (including namespace validation and a demo deployment).

Notes:

  • Only works for public namespaces after the exclusivity period (7 days) has ended.
  • If owner() succeeds (including returning address(0)), Stele does not fall back to getOwner().
  • For private namespaces or during exclusivity, use Option 4 (EIP-1271).

Option 4: Sponsored Registration via EIP-1271

For contracts that implement EIP-1271, someone else can sponsor the name registration. This is the only way for contracts to register names in private namespaces and public namespaces during the exclusivity period.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

contract MyContractWallet {
    using ECDSA for bytes32;
    
    address public owner;
    bytes4 public constant MAGIC_VALUE = bytes4(0x1626ba7e);
    bytes4 public constant INVALID_SIGNATURE = bytes4(0xffffffff);

    constructor(address _owner) {
        owner = _owner;
    }

    /// @notice EIP-1271 function to validate signatures
    /// @param hash The message hash that was signed
    /// @param signature The signature to validate
    /// @return magicValue Returns MAGIC_VALUE if signature is valid
    function isValidSignature(bytes32 hash, bytes memory signature) 
        external 
        view 
        returns (bytes4 magicValue) 
    {
        address signer = hash.recover(signature);
        if (signer == owner) {
            return MAGIC_VALUE;
        }
        return INVALID_SIGNATURE;
    }
}

How it works:

  1. The contract (or its owner) signs an EIP-712 message authorizing the name registration. The recipient in the authorization must be the contract's address itself (the address of the MyContractWallet contract in this example).
  2. A sponsor calls registerNameWithAuthorization on Stele, providing the contract as the recipient.
  3. Stele validates the signature via the contract's isValidSignature function (EIP-1271).
  4. The contract is assigned a name. The sponsor pays the registration fee.

Use cases:

  • Any smart contract that want to be named
  • Contracts that can't send transactions themselves
  • Contracts wanting to register names in private namespaces (required)
  • Allow others to pay for a contract's name registration

Note: The contract must implement EIP-1271's isValidSignature function. The sponsor pays all fees and gas costs. Unlike Options 1 and 2 where receive() is optional (needed only if excess payment is sent), Option 3 does not need a receive() function because any refunds go to the sponsor (the transaction sender), not to the contract.

See MockERC20C and the registerNameWithAuthorizationForERC20C.ts script for an example of how to register a name for an ERC20 token using the EIP-1271 method.

Using Names with Multi-Chain Deployments

If all of the following hold:

  • The same contract is deployed to multiple chains
  • The contract has the same address on those chains (e.g. CREATE2)
  • At least one instance is on Ethereum mainnet

then you can reference the Stele name (e.g. myprotocol@eth) as the identifier for every chain where the address matches the Ethereum deployment. If the address differs on a chain, use the raw address there.

Example:

Network Stele name / Address
Ethereum / Arbitrum / Optimism / Base myprotocol@eth
Avalanche 0x1234…5678

Deployment Pattern

If you use a method like CREATE2 that needs the same bytecode and constructor arguments on every chain to get the same address, constructor registration is possible only if those arguments are identical everywhere (same Stele address, label, and namespace). That works, but it bakes the name into the CREATE2 address, forces dummy name arguments on non-Ethereum chains, and will call Stele on those chains unless you add a chainId check (a call to the Ethereum Stele address on another chain is wasted or can lose ETH).

Prefer a dedicated registerName function that runs only on Ethereum mainnet (chainId = 1) and reverts on other chains. After deployment, call it on Ethereum to attach the name. The CREATE2 address then does not depend on the name, and other chains never touch Stele.

With CREATE, the address does not depend on bytecode, so this pattern is optional.

Example:

contract YourContract {
    // ... Contract code ...
    
    function registerName(string calldata label, string calldata namespace) external payable {
        require(block.chainid == 1, "XNS only available on Ethereum mainnet");
        xns.registerName{value: msg.value}(label, namespace);
    }
}

Note: Add access control as needed.

Contract Ownership Transfer

The Stele contract uses OpenZeppelin's Ownable2Step for 2-step contract ownership transfers. The initial owner is set at deployment and can be transferred using the following process:

Ownership Transfer Process:

  1. Current owner calls transferOwnership(newOwner) to initiate transfer.
  2. Pending owner calls acceptOwnership() to complete transfer.
  3. Only after acceptance does the new owner gain control.

Cancellation:

  • The current owner can cancel a pending transfer by calling transferOwnership(address(0)).
  • Alternatively, the owner can overwrite a pending transfer by calling transferOwnership(differentAddress) again.

Renounce disabled: renounceOwnership() is overridden and always reverts (XNS: renounce disabled) so the protocol cannot become permanently ownerless.

Fee Accounting: Ownership transfers do not migrate already-accrued _pendingFees. Any fees accumulated before acceptOwnership() remain claimable by the previous owner address. Only fees accrued after acceptance are credited to the new owner address.

Example scripts:

Namespace Owner Transfer

Namespace owners can transfer their namespace (and future fee streams) using a 2-step process (transferNamespaceOwnership → acceptNamespaceOwnership), following the same pattern as contract ownership transfer.

Namespace Owner Transfer Process:

  1. Current namespace owner calls transferNamespaceOwnership(namespace, newOwner) to initiate transfer.
  2. Pending namespace owner calls acceptNamespaceOwnership(namespace) to complete transfer.
  3. Only after acceptance does the new namespace owner gain control.

Cancellation:

  • The current namespace owner can cancel a pending transfer by calling transferNamespaceOwnership(namespace, address(0)).
  • Alternatively, the namespace owner can overwrite a pending transfer by calling transferNamespaceOwnership(namespace, differentAddress) again.

Fee Accounting: Namespace owner transfers do not migrate already-accrued _pendingFees. Any fees accumulated before acceptNamespaceOwnership() remain claimable by the previous namespace owner address. Only fees accrued after acceptance are credited to the new namespace owner address.

Example scripts:

Privacy Considerations

Stele names can enhance privacy when used thoughtfully, but the privacy implications depend on your name choice and how you share it.

Off-Chain Communication Benefit

Traditional Ethereum addresses (e.g., 0x8AdEFeb576dcF52F5220709c1B267d89d5208E78) are long hexadecimal strings that are typically shared through digital channels like email, messaging apps, or social media. This creates a digital trail that links your identity to your address, which can be monitored, analyzed, and potentially used for surveillance or correlation attacks.

With Stele, you can share addresses off-chain (verbally or in person) using memorable names like alice@eth or 1x45@eth. The counterparty can easily remember and use the name without needing to copy-paste a long address, reducing digital traces that link your identity to your address.

Name Choice Matters

⚠️ Important: The privacy benefit is conditional and depends on your name choice:

  • ✅ Privacy-enhancing: Using pseudonymous names (e.g., 1x45@eth, alice@eth, crypto123@eth) that don't reveal your real identity, combined with off-chain sharing, can reduce identity-address correlation.
  • ❌ Privacy-reducing: Using identifiable names (e.g., frank-walter@eth, john-smith@eth) that reveal your real identity can actually worsen privacy compared to random addresses, as they create a direct, permanent link between your name and address on-chain.

Best Practices for Privacy-Conscious Users

  • Use pseudonymous or random-looking names that don't reveal your identity
  • Share names off-chain (verbally or in person) when possible
  • Avoid using your real name, email, social media handles (e.g., @yourhandle), or other identifiable information in your Stele name
  • Consider the privacy implications before choosing a name, as names are permanent and non-transferable

License and Deployment Policy

Stele is licensed under the Business Source License 1.1 (BUSL-1.1).

This choice is intentional and motivated by technical and user-safety considerations, not by a desire to restrict innovation.

Why BUSL?

Stele is an identity and naming primitive. Names like alice@x or bankless are meant to be globally unique, permanent, and unambiguous.

Allowing unrestricted third-party deployments of the Stele registry on other chains would lead to:

  • the same name resolving to different addresses on different networks
  • phishing and social-engineering risks
  • user confusion about which contract deployment defines the canonical name-to-address mapping

For identity infrastructure, this is unacceptable.

The BUSL license ensures that:

  • there is a single canonical Stele registry on Ethereum
  • users can safely rely on name-address mappings
  • the mental model of Stele remains simple and trustworthy

What Is Allowed?

  • Reading the code
  • Auditing the code
  • Building tools, wallets, indexers, and integrations
  • Importing and using the public interfaces
  • Non-production and research use

Interfaces and auxiliary files are intentionally kept under permissive licenses (MIT) to enable ecosystem adoption.

Open Source Commitment

In line with BUSL-1.1, this codebase will automatically transition to an open-source license after the specified Change Date.

However, even after the Change Date, Stele remains Ethereum-canonical. The identity, meaning, and trust of Stele names derive from the original deployment on Ethereum mainnet and its immutable history.

Deployments of this code on other chains after the Change Date are not recognized as Stele and do not share any continuity, guarantees, or identity with the canonical registry.

Stele does not support cross-chain name equivalence.

Routes

Routes extend a name with named endpoints, much like paths on a website: label@namespace/routeLabel. Each route points to something different: another EVM address, a Bitcoin or Solana address, a link, or a ready-made transaction. The owner of the name decides what each route points to, and can create as many routes as they like, for free (only gas).

alice@pay/personal  → 0xdf2…3e7     (EVM address)
alice@pay/business  → 0xabc…901     (another EVM address)
alice@pay/bitcoin   → bc1q…         (Bitcoin address)
alice@pay/solana    → 7Ec…          (Solana public key)
alice@pay/pay-rent  → 0xa9059cbb…   (ready-made transaction)
alice@pay/docs      → ipfs://bafy…  (link)

Each route stores two values: bytes target (the endpoint data) and uint32 routeType (how to interpret it, e.g. 0 Ethereum address, 1 Bitcoin address, 2 Solana pubkey, 3 EVM calldata, 4 URI). Route types are an off-chain convention documented in routeTypes, so new formats can be added without changing the contract. Lookups go one way only, from a route to its endpoint.

  • Freeze a route when its endpoint should become permanent. Freezing is irreversible, and only frozen routes resolve: once an application relies on a route, its endpoint can never be swapped.
  • Deactivate a route to take it out of use. Inactive routes stop resolving and can be reactivated later.
  • Close the route book when the set of routes should become permanent: no new route can ever be added.

If you control the name, you control its routes: there are no route NFTs and no separate route owners. Contract, docs and examples: Routes repository.

Full contract API

Functions, events, state variables and types.

API reference