happy in your app becomes happy.brand.eth, and any ENS-aware wallet can send to it, show its avatar, or read its profile.
You don’t move or copy any users. You add one small endpoint, the adapter, on top of the username service you already run. Namespace calls it whenever someone looks up a name under brand.eth. For the product side, read the Universal usernames overview.
How a lookup works
Here is what happens when a wallet resolveshappy.brand.eth:
- The wallet asks ENS. The ENS registry points it to the resolver set on
brand.eth, which is the Namespace Hybrid Resolver with Universal Usernames turned on. - The resolver hands off. It can’t answer from chain data, so it tells the wallet to ask the Namespace gateway instead, using CCIP-Read (EIP-3668). Wallets that support ENS follow this automatically.
- The gateway finds your adapter. It looks up the endpoint URL and optional bearer token saved for
brand.eth. - The gateway calls your adapter. It sends
GET {your endpoint}/happy. - Your adapter answers. It reads
happyfrom your database and returns the profile as JSON: addresses, text records and an optional contenthash. - The gateway signs. It picks the record the wallet asked for, such as the Base address or the avatar, converts it to ENS format and signs it.
- The resolver verifies. The wallet passes the signed answer back to the resolver, which checks the signature and returns the record.
What you’ll set up
The work has two parts:- Build the adapter. Add a route to your existing username service that returns a user’s profile in the format below.
- Connect it in the Namespace App. Save the endpoint, check it with a few real usernames, then set the resolver on
brand.ethso lookups reach the adapter.
Prerequisites
- A second-level
.ethname, such asbrand.eth, owned by a wallet you control. That wallet signs in to the Namespace App and sends the resolver transaction. - A username service you can add a public HTTPS route to.
- Usernames that are valid ENS labels. The gateway only asks for single, ENSIP-15 normalized labels, which means lowercase and no dots. If your usernames allow uppercase, match against a lowercased copy.
Step 1: Build the adapter
The adapter is oneGET route. It takes a username and returns that user’s public profile. Everything below is what the gateway expects from it.
Pick the endpoint URL
You save one base URL per parent name, for examplehttps://api.brand.example/ens. The gateway adds the username as the last path segment. The URL must follow these rules:
https://only.- No username or password, no query string (not even a bare
?), and no fragment. - At most 2048 characters.
- A path is fine. Trailing slashes are removed, so
https://api.brand.example/ens/andhttps://api.brand.example/ensbehave the same.
Handle the request
Forhappy.brand.eth, the gateway sends:
- The last path segment is the username, URL-encoded.
zoëarrives as/ens/zo%C3%AB, so decode it before you look the user up. - The username is already normalized. Compare it against a normalized copy of your usernames, not the casing users typed.
Authorizationis only sent when you saved a bearer token.- Only single labels reach you. Nested names such as
a.happy.brand.eth, and labels that aren’t normalized, get empty records without a call to your adapter.
Return the right status
A
200 response must also:
- Use
Content-Type: application/json. Parameters such as; charset=utf-8are fine. - Be uncompressed. The gateway asks for
identity, and any otherContent-Encodingfails the lookup. Check that your CDN or proxy respects this. - Be at most 262,144 bytes (256 KiB), UTF-8.
- Arrive in full within 5 seconds, DNS lookup included.
Serve it from a public host
- Your hostname must resolve only to public IPv4 or IPv6 addresses. If any answer is private, loopback, link-local or reserved, the gateway refuses to connect.
- TLS is verified against your hostname, so use a valid certificate.
- The gateway calls you server to server, so CORS doesn’t affect resolution. You only need CORS for the endpoint check in the Namespace App.
Return the profile
Every200 response is one profile in schema version 1.
number
required
Must be the number
1. The string "1" fails.string
required
Must equal the requested username exactly, after decoding. If you return
Happy for a request for happy, every lookup for that user fails.Record<string, string>
required
ENS text records, keyed by text record key. Use
{} when there are none. At most 128 keys. See Add text records.Record<string, string>
required
Address records, keyed by ENS coin type as a decimal string. Use
{} when there are none. At most 128 keys. See Add addresses.string
Optional. A content URI such as
ipfs://.... Leave the field out when there is none, because null or "" fails contenthash lookups. See Add a contenthash.Example profile for happy.brand.eth
version, username, the two objects and their sizes) on every lookup. Record values are only checked when a wallet asks for that record. A malformed avatar fails avatar lookups but leaves the user’s addresses working.
Example adapter
This route handler uses the web-standardRequest and Response, so it works as a Next.js App Router route (app/ens/[label]/route.ts) on the Node.js runtime, or in any server that speaks Fetch. Swap findUserByUsername for your own lookup. buildAddresses comes from Build the addresses map.
route.ts
findUserByUsername(label) returns null when there’s no match, and otherwise a user with wallets: Wallet[] plus optional displayName, bio, avatarUrl and twitter. Response.json sets Content-Type: application/json for you.
Test the adapter
Call it the way the gateway will:200, content-type: application/json, no content-encoding header, and a body whose username is happy. An unknown username should return 404:
Step 2: Connect it in the Namespace App
Once the adapter answers correctly, pointbrand.eth at it.
1
Open Universal Usernames
Go to Universal Usernames, connect the wallet that owns
brand.eth, and select the name.2
Enter the endpoint
Enter the base URL, for example
https://api.brand.example/ens. If your adapter requires a bearer token, enter it too. Namespace sends it as Authorization: Bearer <token> and never shows it again after you save.3
Check real usernames
Type a username that exists in your app into Sample username and select Check endpoint. The App fetches
{endpoint}/{username} and shows how many text records and addresses came back. Try a few users, including one with no wallets and one that doesn’t exist, which should report a 404.The check runs in your browser, so your adapter has to allow https://app.namespace.ninja with CORS (Access-Control-Allow-Origin, plus Access-Control-Allow-Headers: Authorization if you use a token). The check is optional, and you can save even if it fails.4
Save
Save the endpoint. The gateway picks up a new or changed endpoint within about 5 seconds.
5
Set the resolver
The App shows the transaction
brand.eth needs:- Set resolver or Update resolver if
brand.ethisn’t on the Namespace Hybrid Resolver yet. If the name already has records, such as its own address or avatar, you can first Transfer records so they keep resolving on the new resolver. - Enable Universal Usernames if
brand.ethis already on the Hybrid Resolver for something else.
username.brand.eth resolves from your adapter.Resolve a name
Resolve one of your users like any ENS name. With viem:resolve.ts
chain: sepolia and a Sepolia RPC URL. A user your adapter returns 404 for resolves to null.
How changes show up
Concurrent lookups for the same user share one request, and one profile answers every record a wallet asks for. Wallets and RPC providers may cache on their side too.
You can change the endpoint or token at any time on the same page, or remove it. If you remove the endpoint while
brand.eth still points to Universal Usernames, its subnames stop resolving until you save a new one.
Add addresses
Addresses are what make a name useful for payments, so this is the part most worth getting right.Key them by coin type
Each key inaddresses is an ENS coin type written as a decimal string, with no leading zeros and no hex. Each value is the address in its usual text format.
- Ethereum mainnet is
60. - Every other EVM chain uses
0x80000000 | chainId(ENSIP-11). Base (chain8453) is2147483648 + 8453 = 2147492101. viem’stoCoinType(chainId)does this math. 2147483648(toCoinType(0)) is the default EVM address. It answers for Ethereum and for any EVM chain that has no key of its own.- Non-EVM chains use their SLIP-44 number (ENSIP-9).
Any EVM chain ID works. For other non-EVM chains, look up the coin type in the address-encoder coin list. An unknown coin type fails lookups for that key.
Use the default EVM address
If a user has the same address on every EVM chain, set only2147483648. When a wallet asks for Ethereum, Base or any other EVM chain and your profile has no key for it, the gateway answers with the default address (ENSIP-19). Add chain-specific keys only where an address differs.
Non-EVM coin types have no fallback. A key that exists but holds an invalid address fails that lookup and never falls back to the default.
Get the format right
The gateway converts each address to ENS binary format with@ensdomains/address-encoder:
- EVM addresses are
0xplus 40 hex characters, EIP-55 checksummed or all lowercase. Mixed case with a wrong checksum fails, so return checksummed addresses (viem’sgetAddressdoes this). - Bitcoin addresses must be mainnet. Testnet addresses such as
tb1...fail. - Solana addresses are the base58 public key.
- Each value can be up to 1024 characters.
Build the addresses map
This module turns a user’s wallets into theaddresses object. It validates each address with the same library the gateway uses, so a bad address shows up in your logs instead of breaking a lookup.
addresses.ts
buildAddresses throws on an invalid address. Catch the error where you build the profile if you’d rather drop one bad wallet than fail the whole response.
Add text records
texts holds ENSIP-5 text records. Wallets and apps read the keys they support, so stick to the common ones:
Keys are case-sensitive. Don’t put a display name in
display, because ENSIP-5 reserves that key for the name itself in a preferred casing.
Limits: up to 128 keys, each key 1 to 256 characters, each value up to 4096 characters. Values must be strings, so a number or null fails lookups for that key. Leave a key out when the user has no value.
Add a contenthash
contenthash links the name to a website or file, so happy.brand.eth can open in browsers that support ENS sites. It must be a URI with one of these schemes (ENSIP-7):
A bare CID without a scheme, an
https:// URL, or any other scheme fails contenthash lookups. Maximum length is 4096 characters.
Pros and cons
Universal Usernames fits when you already run a username system and want it to work outside your app. It isn’t the right choice for every product. What you get:- Every existing username resolves as soon as the resolver is set. Nothing is minted, copied or kept in sync.
- One transaction on
brand.ethcovers every username, current and future. - Users change their avatar or add a wallet in your app, and wallets see it within 60 seconds.
- Ethereum, any EVM chain, Bitcoin, Solana and other chains sit side by side under the same name.
- Wallets, explorers and apps that resolve ENS names with CCIP-Read need no integration with you.
- Your uptime is the name’s uptime. If your adapter is down, slow or returns bad JSON, lookups for your users fail. There’s no fallback copy.
- Wallets trust the gateway’s signature, not a proof from your database. This is the same trust model as other offchain ENS names.
- Users don’t own the names onchain. The name follows your database, so users can’t transfer it or hold it as an NFT, and you can rename or remove it at any time.
- Universal Usernames replaces offchain or L2 subnames under the same parent name.
- The gateway answers address, text and contenthash records. Other ENS record types aren’t served.
- Most major wallets follow CCIP-Read, but not every app does. Test the ones your users rely on.
Security practices
Your adapter now decides where money goes when someone sends tohappy.brand.eth. Treat it like a payments endpoint.
- Publish only what users chose to share. Anyone can resolve any username and read every record in its profile. Leave out private fields, internal IDs and unverified data.
- Hide accounts that shouldn’t be found. Anyone can guess usernames and see which ones resolve. Return
404for private, suspended or deleted accounts. - Serve only addresses the user controls. A wrong address means lost funds. Store addresses the user has proven they own, for example with a signature, and never fill in an address on their behalf.
- Don’t recycle usernames quickly. If
happyis deleted and someone else claims it, payments sent tohappy.brand.ethgo to the new user. Keep released usernames reserved for a while, and remember the 60-second cache. - Require a bearer token. It lets your adapter reject callers other than Namespace. Compare it in constant time, as in the example adapter. Namespace stores it encrypted, bound to your parent name, and sends it only in the
Authorizationheader to your endpoint. It can be up to 4096 characters. - Rotate the token without downtime. Make your adapter accept both the old and new tokens, save the new one in the App, wait about 5 seconds, then drop the old one.
- Re-enter the token when you move hosts. If you change the endpoint’s scheme, host or port, the App drops the saved token unless you enter it again. That stops a token from following a changed endpoint to a server it wasn’t meant for.
- Protect the wallet that owns
brand.eth. Only that wallet can save, change or remove the endpoint and set the resolver, and API keys can’t. Whoever holds it can redirect every username. Use a hardware wallet or a multisig, and know that the endpoint control moves with the name if it changes hands. - Rate-limit with care. A
429fails the lookup. Set limits that fit your real lookup volume rather than blocking the gateway. - Keep usernames out of your logs if you can. Namespace’s gateway logs record the parent name, a hash of the username, the record type, the outcome and latency. They never contain usernames, record values, profile bodies or the token.
Next steps
Universal usernames overview
Why Universal Usernames exist and what to decide before you build.
Open Universal Usernames
Save your endpoint and set the resolver in the Namespace App.
Create offchain subnames
Let Namespace store the names and records instead of your own endpoint.
Talk to us
Bring your existing naming service and we’ll map it onto ENS with you.

