Asentum

Build Apps

Names for Developers

@usernames from code · @asentum/sdk 0.3.1 · Estimated read time: 10 minutes

Updated 2026-11-07: names are live since block 1,200,000; the SDK has write methods for every registry call; marketplace section added.

How names work

A native name such as @alice points at an address. Names are stored in the NameRegistry, a JavaScript system contract that is part of the chain at ase1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqty736ey. It has been live since block 1,200,000 (7 November 2026) on the testnet, chain 1423.

  • Every write (register, transfer, primary, records) is an ordinary signed contract call to the registry.
  • Every read is a free view call, or one of two node lookups.
  • The registry checks ownership with msg.sender, so the owner of a name can be a wallet or a contract.
  • There is no approve or operator method. Only the current owner can move a name. Marketplaces hold names in escrow instead: see Building a names marketplace.

For the user side (what a name is, the reserved list, registering in the explorer) see Native Usernames.

Rules and fees

  • 3 to 24 characters: a-z, 0-9 and the hyphen. No hyphen at the start or end, no double hyphen, ASCII only, and nothing that starts with 0x or ase1.
  • Input is case-insensitive and may carry one leading @. normalizeName gives the canonical form.
  • An address can register at most 5 names per 43,200 blocks. capOf(address) shows how many are used.
  • The first name an address gets becomes its primary name.
  • Some names are reserved. Only the registrar can release them. isReserved(name) tells you.
LengthFee
3 characters500 ASE
4 characters100 ASE
5 characters50 ASE
6 or more10 ASE

The value of register must be exactly the fee, or the call reverts. Half is burned and half goes to the treasury. Transfers, primary changes and records cost only gas, and every method except register rejects a non-zero value.

Setup

The reads below need @asentum/sdk 0.3.0. The wallet write methods (registerName, transferName, setPrimaryName, clearPrimaryName, setNameRecord) arrived in 0.3.1. With an older SDK, send the same calls with wallet.sendCall.

npm install @asentum/sdk@^0.3.1

import { AsentumClient, AsentumWallet } from '@asentum/sdk';

const client = new AsentumClient('https://testnet.asentum.com');
const wallet = AsentumWallet.fromSecretKey(client, SECRET_KEY_HEX, PUBLIC_KEY_HEX);

Every write method takes the usual send options (gasLimit, nonce, signal) and returns a SentTx. Call .wait() for the receipt and check receipt.success: a revert such as "name is taken" shows up there, not as a thrown error. An invalid name throws InvalidNameError before anything is signed. See the SDK reference for the full list.

Reading names

import { NAME_REGISTRY_CONTRACT, formatName, toAse1 } from '@asentum/sdk';

// false before block 1,200,000 and on nodes older than 0.9.0
if (await client.namesActive()) {
  const owner = await client.resolveName('@alice');        // ase1 owner, or null when free
  const primary = await client.lookupAddress(wallet.address); // "alice" or null
  if (primary) console.log(formatName(primary));          // "@alice"

  // A "to" field: "@name" is resolved, an address passes through.
  const { address, name } = await client.resolveRecipient('@alice');

  // The full record (owner, registeredAt, primary flag, records) is a view call.
  const rec = await client.view(NAME_REGISTRY_CONTRACT, 'get', ['alice']);
  if (rec) console.log(toAse1(rec.owner), rec.records.x);  // owner comes back as hex

  // Free to register? reason is "taken", "reserved", a broken rule, or null.
  const info = await client.view(NAME_REGISTRY_CONTRACT, 'isAvailable', ['alice']);
}
CallReturns
client.namesActive()true once the node serves names. False before the flag day and on nodes older than 0.9.0.
client.resolveName(name)The ase1 owner, or null when the name is free.
client.getNameLookup(name)The full GET /name/<name> body: { active, name, address, ase1, blockNumber }.
client.lookupAddress(address)The primary name of an address without the @, or null.
client.resolveRecipient(input){ address, name } for a "to" field. Throws NameNotFoundError for a free name.
view get(name){ name, owner, registeredAt, primary, records } or null. owner is lowercase hex.
view isAvailable(name){ name, available, reason, fee }. fee is in wei.
view primaryOf(address), namesOfCount(address), capOf(address), isReserved(name), params(), stats()See the contract methods on the Native Usernames page.

Primary names can change in any block. Cache them for display for a minute at most, and resolve again right before you send.

Registering

import { normalizeName, nameFeeWei, formatAse } from '@asentum/sdk';

const name = normalizeName('@MyName');                  // "myname", or null if invalid
console.log('fee', formatAse(nameFeeWei(name)), 'ASE'); // 10 ASE for 6+ characters

const tx = await wallet.registerName(name);             // sends exactly nameFeeWei(name)
const receipt = await tx.wait();
if (!receipt.success) throw new Error(receipt.error);   // e.g. "name is taken"
  • registerName sends exactly nameFeeWei(name), so you never compute the value yourself.
  • Check isAvailable first to show the user why a name is not free (taken, reserved, or which rule it breaks).
  • A sixth registration inside 43,200 blocks reverts. Use capOf(address) to show the limit before the user pays gas.

Transferring

transferName(name, to) calls transfer(name, to) on the registry. Only the owner can call it and there is no fee. The recipient can be an ase1 address (the SDK converts it) or a contract.

const tx = await wallet.transferName('myname', 'ase1wtc569gfwk87hplaawr46uj4suwsx2wndgf8ue');
const receipt = await tx.wait();
if (!receipt.success) throw new Error(receipt.error);   // e.g. "you do not own this name"
  • If the name was the sender's primary name, the sender is left with no primary name.
  • The records on the name (avatar, url, x, description) are cleared. The new owner starts clean.
  • The name does not become the recipient's primary name. They call setPrimaryName themselves.
  • Transferring to yourself reverts.
  • A transfer is final. There is no undo, so check the recipient before you send. Sending to a contract that has no way to send the name back locks it there for good.

Primary names

The primary name is the one apps show for an address. An address has at most one.

await (await wallet.setPrimaryName('myname')).wait(); // must be a name you own
await (await wallet.clearPrimaryName()).wait();        // show no name; you keep the name

Records

The owner can attach text records to a name. Keys are fixed:

KeyValue
avatarAn image link starting with https://, http:// or ipfs:// (up to 256 characters)
urlA link starting with https://, http:// or ipfs:// (up to 256 characters)
xAn X handle (up to 15 characters; a leading @ is dropped)
descriptionA short bio (up to 160 characters)
await (await wallet.setNameRecord('myname', 'avatar', 'ipfs://bafy...')).wait();
await (await wallet.setNameRecord('myname', 'url', 'https://example.com')).wait();
await (await wallet.setNameRecord('myname', 'x', '@asentum')).wait();
await (await wallet.setNameRecord('myname', 'description', 'Builder on Asentum')).wait();
await (await wallet.setNameRecord('myname', 'x', '')).wait();  // '' clears the record

Read them from get(name).records. Records are text the owner wrote: render them as plain text and treat links as untrusted.

From a dapp (@asentum/connect)

In a browser dapp the user's wallet signs, so use @asentum/connect 0.2.0. It has registerName, setPrimaryName, nameAvailability, resolveRecipient and primaryName on the client, and usePrimaryName and useNamesActive hooks. For transfers and records, call the registry through useContract.

import { useAsentum, useContract, useWallet, usePrimaryName,
         NAME_REGISTRY_CONTRACT } from '@asentum/connect';

function Names() {
  const client = useAsentum();
  const { name } = useWallet();                  // the connected account's primary name, or null
  const registry = useContract(NAME_REGISTRY_CONTRACT);

  async function register(input) {
    const a = await client.nameAvailability(input); // { available, reason, feeWei }
    if (!a.available) throw new Error(a.reason);
    const txHash = await client.registerName(input); // the wallet asks the user; sends the exact fee
    return client.waitReceipt(txHash);
  }

  // The kit has no transferName or setNameRecord helper: call the registry directly.
  const transfer = (n, to) => registry.call('transfer', [n, to], '0', { gasLimit: 300000 });
  const setRecord = (n, k, v) => registry.call('setRecord', [n, k, v], '0', { gasLimit: 300000 });
}

Node lookups

Without an SDK, any node from release 0.9.0 answers two GET routes. Both accept ?blockTag=finalized.

RouteReturns
GET /name/<name>The owner of a name. address and ase1 are null when the name is free.
GET /name-of/<address>The primary name of an address (ase1 or 0x). name is null when it has none.
GET https://testnet.asentum.com/name/alice
{ "active": true, "name": "alice", "address": "0xa1a1...", "ase1": "ase15xs6...yggla5", "blockNumber": "1200345" }

GET https://testnet.asentum.com/name-of/ase15xs6rgdp5xs6rgdp5xs6rgdp5xs6rgdpyggla5
{ "active": true, "address": "0xa1a1...", "ase1": "ase15xs6...yggla5", "name": "alice", "blockNumber": "1200345" }

Before activation the body says active: false; an older node answers 404. The JSON-RPC methods ase_resolveName and ase_lookupAddress return the same bodies. Details are in the Node API reference.

Names inside contracts

A contract calls the registry like any other contract, with E(address).method(args). E() takes a hex address, so the registry appears in contract code as 0x000000000000000000000000000000000000000b, its hex form. msg.sender and the owner field are both lowercase hex, so they compare directly. See Writing Contracts for the rest of the contract API.

// E() takes hex addresses, so a contract refers to the registry by its hex form.
const REGISTRY = '0x000000000000000000000000000000000000000b';

({
  // Only lets a caller in if they own the given name.
  greet(name) {
    const rec = E(REGISTRY).get(name);   // { name, owner, registeredAt, primary, records } or null
    assert(rec && rec.owner === msg.sender, 'you do not own @' + name);
    emit('Greeted', { name: rec.name, owner: msg.sender });
  },

  // The primary name of the caller, or null.
  whoAmI() {
    return E(REGISTRY).primaryOf(msg.sender);
  },
});

When your contract calls E(REGISTRY).register(name) or E(REGISTRY).transfer(name, to), the registry sees your contract as msg.sender. That is how a contract owns names and moves them. To register from a contract, attach the fee: E(REGISTRY, { value: fee }).register(name).

Building a names marketplace

Since nobody but the owner can move a name, a marketplace cannot sell a name the seller still holds. It works as an escrow instead: the seller hands the name to the marketplace contract, and the marketplace hands it to the buyer when they pay.

The flow

  1. Prepare. The seller calls marketplace.prepare(name, price). The marketplace checks E(registry).get(name).owner === msg.sender and records the seller.
  2. Deposit. The seller calls wallet.transferName(name, marketplaceAddress).
  3. Activate. The seller calls marketplace.activate(name), which checks that the marketplace now owns the name and opens the listing.
  4. Buy. A buyer calls marketplace.buy(name) with the price as the value. The marketplace pays the seller and calls E(registry).transfer(name, buyer).
  5. Cancel. The seller calls marketplace.cancel(name) and gets the name back through the same transfer.

Prepare must come before the deposit. The marketplace learns who the seller is while the seller still owns the name, so nobody else can claim a name that arrives later. Records are cleared on every transfer, so the buyer starts with a clean profile.

Example contract

// A names marketplace. The registry has no approve, so the marketplace holds
// each listed name in escrow and moves it with E(REGISTRY).transfer(...).
const REGISTRY = '0x000000000000000000000000000000000000000b';
const PREPARE_WINDOW = 300n; // blocks between prepare and activate

function load(name) {
  const raw = storage.get('l:' + name);
  return raw ? JSON.parse(raw) : null;
}
function save(name, listing) {
  storage.set('l:' + name, JSON.stringify(listing));
}
function lookup(name) {
  const rec = E(REGISTRY).get(name); // also normalises the name ("@Alice" -> "alice")
  assert(rec, 'name is not registered');
  return rec;
}

({
  // 1. The seller, still the owner, announces the sale.
  prepare(name, price) {
    assert(BigInt(msg.value) === 0n, 'this method does not accept ASE');
    const rec = lookup(name);
    assert(rec.owner === msg.sender, 'you do not own this name');
    const wei = BigInt(price);
    assert(wei > 0n, 'price must be above zero');
    save(rec.name, { seller: msg.sender, price: wei.toString(), at: chain.blockNumber.toString(), active: false });
    emit('Prepared', { name: rec.name, seller: msg.sender, price: wei.toString() });
    return rec.name;
  },

  // 2. (outside) The seller calls transfer(name, marketplace) on the registry.

  // 3. Open the listing once the marketplace owns the name.
  activate(name) {
    assert(BigInt(msg.value) === 0n, 'this method does not accept ASE');
    const rec = lookup(name);
    const l = load(rec.name);
    assert(l && !l.active, 'call prepare first');
    assert(msg.sender === l.seller, 'only the seller can activate');
    assert(chain.blockNumber - BigInt(l.at) <= PREPARE_WINDOW, 'prepare expired: cancel and prepare again');
    assert(rec.owner === msg.contractAddress, 'transfer the name to the marketplace first');
    l.active = true;
    save(rec.name, l);
    emit('Listed', { name: rec.name, seller: l.seller, price: l.price });
  },

  // 4. Pay exactly the price and receive the name.
  buy(name) {
    const rec = lookup(name);
    const l = load(rec.name);
    assert(l && l.active, 'not for sale');
    assert(BigInt(msg.value) === BigInt(l.price), 'send exactly the price');
    assert(msg.sender !== l.seller, 'you are the seller');
    storage.delete('l:' + rec.name);
    E(REGISTRY).transfer(rec.name, msg.sender); // records are cleared for the buyer
    transfer(l.seller, BigInt(l.price));         // pay the seller
    emit('Sold', { name: rec.name, seller: l.seller, buyer: msg.sender, price: l.price });
  },

  // 5. The seller takes the name back (or drops a prepare).
  cancel(name) {
    assert(BigInt(msg.value) === 0n, 'this method does not accept ASE');
    const rec = lookup(name);
    const l = load(rec.name);
    assert(l, 'no listing');
    assert(msg.sender === l.seller, 'only the seller can cancel');
    storage.delete('l:' + rec.name);
    if (rec.owner === msg.contractAddress) E(REGISTRY).transfer(rec.name, l.seller);
    emit('Cancelled', { name: rec.name, seller: l.seller });
  },

  listing(name) {
    const rec = E(REGISTRY).get(name);
    return rec ? load(rec.name) : null;
  },
});
  • The prepare window. activate only works within 300 blocks of prepare and only for the seller who prepared. A seller who sells the name elsewhere after preparing cannot later activate a listing on a name somebody else deposited. If the window runs out, cancel still returns a deposited name.
  • A name sent without prepare is stuck. The contract has no listing for it and so no seller to return it to. Tell users in your UI to always go through the three steps, or add an owner-controlled rescue method.
  • Exact payment. buy takes exactly the price, so no change needs refunding. transfer(seller, amount) is the built-in ASE send, settled after the call returns.
  • Everything reverts together. If the registry transfer fails, the payment and the storage changes in buy are rolled back with it.
  • Add a marketplace fee by keeping part of msg.value before paying the seller.

Calling it from the SDK

const MARKET = 'ase1...your-marketplace';
const price = 25n * 10n ** 18n; // 25 ASE

// Seller: three transactions, in this order.
await (await seller.sendCall({ to: MARKET, method: 'prepare', args: ['myname', price.toString()] })).wait();
await (await seller.transferName('myname', MARKET)).wait();
await (await seller.sendCall({ to: MARKET, method: 'activate', args: ['myname'] })).wait();

// Buyer: one transaction with the price as the value.
const tx = await buyer.sendCall({ to: MARKET, method: 'buy', args: ['myname'], valueWei: price });
const receipt = await tx.wait();
if (!receipt.success) throw new Error(receipt.error);

The SDK turns ase1 addresses in arguments into hex before signing, so pass ase1 everywhere on the client side. Show users the listing with a free view call: client.view(MARKET, 'listing', ['myname']).

Errors and gotchas

ErrorWhen
NamesNotActiveErrorBefore block 1,200,000, or the node is older than 0.9.0. Hide name features and show addresses.
NameNotFoundErrorresolveRecipient got a valid name that nobody owns.
InvalidNameErrorThe input breaks a naming rule. Thrown before any network call or signature.
  • Reverts are in the receipt. "name is taken", "name is reserved", "you do not own this name" and the fee message come back as receipt.error with success: false.
  • Contract code uses hex. E() and the registry's transfer refuse ase1 strings inside a contract. Off chain, the SDK and connect convert ase1 for you.
  • Show both. On a confirm screen show the @name and the ase1 address it resolved to.
  • Testnet names do not carry over to mainnet.

Read next