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.
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
getNameandgetAddressfunction. - 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:
registerNameonly works for public namespaces after the exclusivity period (7 days) has ended. During the exclusivity period or for private namespaces, namespace owners must useregisterNameWithAuthorization, even for their own registrations.
Example scripts:
- Name registration for EOA
- Name registration for ERC20 token (via constructor)
- Name registration for ERC20 token (via separate
registerNamefunction)
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:
- The recipient signs an EIP-712 message authorizing a specific name registration (label, namespace, recipient address, and
validUntilexpiry). - The sponsor calls
registerNameWithAuthorizationwith the recipient's signature and pays the registration fee. - 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
batchRegisterNameWithAuthorizationto register multiple names within the same namespace in a single transaction. See the API documentation for details.
Example scripts:
- Name registration with authorization for EOA
- Name registration with authorization for ERC20 token
- Batch name registration with authorization
Name Resolution
Stele provides simple on-chain resolution for names and addresses.
Look up Address from Name: getAddress
- Resolve a name like
vitalik@001orbob@ethto its Ethereum address. - Strings without an
@namespace separator are invalid and returnaddress(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:
- 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).
- Register Namespace: Submit a transaction with the required ETH to register the namespace (see
registerPublicNamespacein 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:
- 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).
- Register Namespace: Submit a transaction with the required ETH to register the namespace (see
registerPrivateNamespacein 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:
Regular users always pay the standard fees when registering namespaces via
registerPublicNamespaceorregisterPrivateNamespace.During the onboarding period (one year after Stele v1 contract deployment on 28 Jan 2026), the Stele contract owner can register public and private namespaces at no cost using
registerPublicNamespaceForandregisterPrivateNamespaceFor, respectively, to foster adoption. These are OWNER-only functions that allow registering namespaces for other addresses during the onboarding period.Security recommendation: Namespace owners should consider using multisig wallets to reduce the risk of wallet access loss or compromise. This is especially important for public namespace owners who receive ongoing fee rewards, and for private namespace owners who maintain exclusive control over their namespace.
Example scripts:
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:
- Check pending fees for any address (
getPendingFees) - Claim fees to caller (
claimFeesToSelf) - Claim fees to a different recipient (
claimFees)
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 areceive()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
registerNamefunction 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 areceive()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 callingregisterName. - The
registerNamefunction 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 returningaddress(0)), Stele does not fall back togetOwner(). - 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:
- 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
MyContractWalletcontract in this example). - A sponsor calls
registerNameWithAuthorizationon Stele, providing the contract as the recipient. - Stele validates the signature via the contract's
isValidSignaturefunction (EIP-1271). - 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:
- Current owner calls
transferOwnership(newOwner)to initiate transfer. - Pending owner calls
acceptOwnership()to complete transfer. - 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:
- Current namespace owner calls
transferNamespaceOwnership(namespace, newOwner)to initiate transfer. - Pending namespace owner calls
acceptNamespaceOwnership(namespace)to complete transfer. - 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:
- Query pending namespace owner
- Initiate namespace ownership transfer
- Accept namespace ownership transfer
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.