Skip to main content
Your users already have usernames in your app. Universal Usernames gives each of them a matching ENS name: 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 resolves happy.brand.eth:
  1. 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.
  2. 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.
  3. The gateway finds your adapter. It looks up the endpoint URL and optional bearer token saved for brand.eth.
  4. The gateway calls your adapter. It sends GET {your endpoint}/happy.
  5. Your adapter answers. It reads happy from your database and returns the profile as JSON: addresses, text records and an optional contenthash.
  6. 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.
  7. The resolver verifies. The wallet passes the signed answer back to the resolver, which checks the signature and returns the record.
Your database stays the source of truth. Namespace stores your endpoint URL and token, never your users or their records.

What you’ll set up

The work has two parts:
  1. Build the adapter. Add a route to your existing username service that returns a user’s profile in the format below.
  2. Connect it in the Namespace App. Save the endpoint, check it with a few real usernames, then set the resolver on brand.eth so lookups reach the adapter.
After that, every username your adapter knows resolves as an ENS name, and there’s nothing to mint or sync.

Prerequisites

  • A second-level .eth name, such as brand.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 one GET route. It takes a username and returns that user’s public profile. Everything below is what the gateway expects from it.
In the Namespace App, the Build with AI tab on the Universal Usernames page has a prompt you can paste into a coding agent. It describes the same contract as this section.

Pick the endpoint URL

You save one base URL per parent name, for example https://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/ and https://api.brand.example/ens behave the same.

Handle the request

For happy.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.
  • Authorization is 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-8 are fine.
  • Be uncompressed. The gateway asks for identity, and any other Content-Encoding fails 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

Every 200 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.
Any other top-level field fails the whole profile.
Example profile for happy.brand.eth
The gateway checks the outer shape (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-standard Request 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:
Expect 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, point brand.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.eth isn’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.eth is already on the Hybrid Resolver for something else.
Send the transaction. When the step shows Resolver set and configured, every username.brand.eth resolves from your adapter.
brand.eth serves one kind of subname at a time. If it serves offchain subnames or L2 subnames today, those stop resolving once you switch, and your adapter answers for every subname instead. The App warns you before the switch.

Resolve a name

Resolve one of your users like any ENS name. With viem:
resolve.ts
For a Sepolia name, use 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 in addresses 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 (chain 8453) is 2147483648 + 8453 = 2147492101. viem’s toCoinType(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 only 2147483648. 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 0x plus 40 hex characters, EIP-55 checksummed or all lowercase. Mixed case with a wrong checksum fails, so return checksummed addresses (viem’s getAddress does 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 the addresses 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
For these wallets:
the result is:
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.eth covers 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.
What to weigh:
  • 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.
If you’d rather Namespace store the names and records for you, use offchain subnames. If users should own their names onchain, use L1 or L2 subnames.

Security practices

Your adapter now decides where money goes when someone sends to happy.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 404 for 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 happy is deleted and someone else claims it, payments sent to happy.brand.eth go 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 Authorization header 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 429 fails 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.