documentation

how .fomo names work.

every .fomo name is an NFT on Robinhood Chain. the contract is the registry, the website is one interface to it, and the resolver API is a cached read of the chain that any app can call with a single HTTP request.

quickstart

$ curl https://fomodomains.com/api/resolve/alice.fomo

a registered name returns 200 with the owner, the target address and the token id. an unregistered one returns 404. that is the whole integration surface for most applications.

CALLFomoNames.resolve("alice")

no server at all? call the contract. resolve(string) returns (owner, target) straight from Robinhood Chain (chain id 4663). the api above is exactly this call, cached at the edge.

contract  0xeAd677b6e4500D013bc8BD6290f8d375ED5F525C
chain     Robinhood Chain · id 4663 · rpc https://rpc.mainnet.chain.robinhood.com
standard  ERC-721 + ERC-721Enumerable + ERC-721Metadata

architecture

three layers, and only the first one holds any state.

1 · the contract is the registry

FomoNames is a single ERC-721 contract. a name is a token whose id is keccak256(name), so the id is derived from the name itself and nothing else: no counter, no admin assignment, no way for two contracts to disagree on what "alice" is. each token carries three things on-chain: the owner (standard ERC-721 ownership), the target (the address the name resolves to) and an optional salePrice. registering, repointing, transferring, listing and buying are all functions of this one contract, executed by the user's own wallet. the website never holds a key that can touch a name.

2 · the website is an interface

fomodomains.com builds the transactions and sends them to your wallet. before every write it simulates the call against the chain, so a name that is taken or a price that changed shows up as a clear error before you sign anything. the site can go offline and every name keeps working: it is visible in your wallet, on Blockscout, and resolvable by anyone reading the contract.

3 · the api is a cached read of the chain

the read endpoints below are served from the contract's own view functions, batched over JSON-RPC, cached fifteen seconds in memory and sixty seconds at the edge. registration and sale dates come from the contract's events (the block timestamp of Registered, Migrated, Listed and Sold). if the public RPC drops a call, the api retries on fallback endpoints and, as a last resort, serves its last known answer rather than an error. there is no database in the read path.

anatomy of a name

tokenId    = uint256(keccak256("alice"))          // derived, deterministic
owner      = ERC-721 owner                           // who controls it
target     = address the name resolves to            // defaults to owner, changeable
salePrice  = 0 (not for sale) or the price in wei    // marketplace state, on-chain
tokenURI   = data:application/json;base64,…           // metadata + SVG generated by the contract

tokenURI is computed on-chain: the JSON and the SVG image are built by the contract from the name and the current target. nothing is hosted on IPFS or on a server, so what a wallet displays cannot rot or be swapped later.

guarantees

what the contract makes impossible, not just unlikely. all of it is enforced by code you can read on Blockscout.

  • no expiry, by construction — there is no expiry field, no renewal function and no admin path that burns a token. a name changes hands only through an ERC-721 transfer or a sale, both initiated by its owner.
  • no free minting, ever again — the only way to create a name is register() with exactly the price attached. the admin migrate() path used once to move the pre-contract names is closed permanently: migrationOpen() is false and has no setter that turns it back on.
  • atomic salesbuy() moves the NFT to the buyer and pays the seller (minus the fee) in the same transaction, guarded against reentrancy. either everything happens or nothing does. no escrow, no deposit, no "we'll settle later". if the seller's address refuses ETH, the purchase reverts and the name stays put.
  • exact amountsregister and buy require msg.value to equal the price to the wei. overpaying reverts, so a wallet bug cannot lose funds. the marketplace fee is capped at 10 % in the contract; today it is 2.5 %.
  • listings cannot go stale — any transfer clears the sale price and repoints the target to the new owner inside the transfer itself. a buyer can never receive a name that still points at the previous owner, and a sold name can never be bought twice.
  • validation on-chain — the character rules below are checked by the contract, byte by byte, not by the website. a look-alike name with a non-ASCII character cannot exist on-chain, whatever front-end sent it.
  • transparent admin — the owner wallet can change the price, the fee (≤ 10 %), the treasury and the reserved list. every such change emits an event, and ownership transfers use a two-step accept so a typo cannot lose the contract.

read endpoints

public, CORS-open, no authentication. rate limited per IP (120 reads per minute) with Retry-After on 429. add ?fresh=1 right after your own transaction to bypass the caches.

GET/api/resolve/{domain}

resolve a domain

the record behind a name, read from the contract. with or without the TLD, case-insensitive. addresses are lowercase 0x hex. 404 when unregistered.

{
  "name": "alice",
  "fqdn": "alice.fomo",
  "tld": "fomo",
  "owner": "0x8f2a…c41d",
  "target": "0x93b7…7e02",
  "tokenId": "3511…5549",
  "registeredAt": "2026-09-16T11:02:44.000Z",
  "txHash": "0x7d3e…",
  "migrated": false,
  "forSale": null
}

txHash is the mint transaction. migrated is true for the names that existed before the contract; their registeredAt and txHash are those of their original registration. forSale carries priceWei and priceEth when the name is listed.

GET/api/check?name={name}

check availability

validates the name and asks the contract whether it is free. always 200; read the available flag. the contract is the only judge: a name reported free can still be taken by a transaction mined before yours.

{ "available": true, "valid": true, "normalized": "alice", "fqdn": "alice.fomo", "registered": false, "priceEth": "0.005", "priceWei": "5000000000000000" }
GET/api/domains?limit=50&offset=0&q=

list the registry

every name, newest first, each with the fields of resolve. limit caps at 100; q is a case-insensitive substring filter.

GET/api/wallet/{address}

domains by wallet

reverse lookup through recordsOf(address): everything an address owns, newest first. served no-store.

GET/api/market/listings?limit=50&offset=0&q=&sort=newest

names for sale

every token whose salePrice is non-zero, with seller, priceEth and listedAt. sort is newest, price-asc or price-desc.

GET/api/market/{domain} · /api/market/sales

one listing · recent sales

a single listing (404 NOT_LISTED otherwise), and the sale history rebuilt from Sold events: buyer, seller, price, fee, txHash and soldAt.

GET/api/stats

registry stats

total names, current price, market fee, treasury, chain id, contract address and the RPC the site uses. "mode": "onchain".

the contract

what your wallet actually sends. every function below is external on FomoNames; the website is just a convenient way to call them.

TXregister(string name) payable

register

attach exactly price() wei. the contract validates the name, requires that the token does not exist, records the name, sets the target to the sender, mints the token to the sender with _safeMint and forwards the payment to the treasury. one transaction, one block, and the name is in your wallet. reverts with InvalidName, NameReserved, NameTaken or WrongValue(expected, sent).

TXsetTarget(string name, address target)

repoint

the owner, or an address approved for the token, points the name at any wallet. this is what the resolver returns. ownership does not move.

TXtransferFrom · safeTransferFrom(from, to, tokenId)

transfer

standard ERC-721. works from the website, from Blockscout, from any wallet or marketplace that speaks ERC-721. inside the transfer the contract clears any listing and repoints the target to the new owner.

TXlist(string name, uint256 price) · unlist(string name)

sell

the owner sets a sale price (minimum 0.001 ETH) or clears it. relisting with a new price is a single call. a listing is state on the token, not an order on a server: it survives the website and is visible to any indexer.

TXbuy(string name) payable

buy

attach exactly the listed price. the contract moves the token to you (clearing the listing and repointing the target), sends the fee to the treasury and the rest to the seller, and emits Sold. all in one transaction under a reentrancy guard. reverts with NotForSale, WrongValue, SelfPurchase or TransferFailed if the seller cannot receive ETH.

fee     = price × feeBps / 10 000          // feeBps ≤ 1000, currently 250 (2.5 %)
seller  ← price − fee
treasury ← fee
CALLresolve · record · recordById · records(offset, limit) · recordsOf(owner) · available · exists · tokenIdOf · nameOf · tokenURI

views

everything the api exposes is a view on the contract. records paginates the whole registry in mint order, recordsOf lists an owner's names, record returns the struct { tokenId, name, owner, target, salePrice }. build your own indexer, explorer or wallet plugin without asking anyone.

events & indexing

Registered(uint256 indexed tokenId, string name, address indexed owner, uint256 paid)
Migrated(uint256 indexed tokenId, string name, address indexed owner, address target)
TargetSet(uint256 indexed tokenId, address indexed target)
Listed(uint256 indexed tokenId, address indexed seller, uint256 price)
Unlisted(uint256 indexed tokenId)
Sold(uint256 indexed tokenId, address indexed seller, address indexed buyer, uint256 price, uint256 fee)
Transfer(address indexed from, address indexed to, uint256 indexed tokenId)   // ERC-721

the contract was deployed at block 64700469; scan from there. the website's own api is built from exactly these events plus the view functions, so anything it shows, you can rebuild.

ens & wallets

every .fomo name is also reachable as name.fomodomains.eth, so you can paste it into the recipient field of Rabby, MetaMask, Robinhood Wallet or any app that resolves ENS. no installation, no plugin.

how it works

fomodomains.eth is owned by the project treasury on Ethereum and its resolver is FomoEnsResolver (0x0428F80f58Fece992678557aD3Ac23d9A5162434), a wildcard resolver (ENSIP-10) that answers every subname through CCIP-Read (EIP-3668). when a wallet asks for alice.fomodomains.eth, the resolver tells it to fetch https://fomodomains.com/api/ens/{sender}/{data}.json; the gateway reads alice's target from the FomoNames contract on Robinhood Chain, signs the answer with a key the resolver trusts, and the wallet verifies that signature on Ethereum before using the address. a forged or expired answer is rejected on-chain.

alice.fomodomains.eth
  → ENS registry (Ethereum): resolver = FomoEnsResolver
  → resolver: OffchainLookup → https://fomodomains.com/api/ens/…
  → gateway: FomoNames.resolve("alice") on Robinhood Chain, signed
  → resolver.resolveWithProof(): signature ok → address
GET/api/ens/{sender}/{data}.json · POST/api/ens

the gateway

standard EIP-3668 gateway. sender must be the resolver, data the resolve(bytes name, bytes data) call. answers addr(node), addr(node, 60) (and the ENSIP-11 coin type of Robinhood Chain), text(node, "url" | "description"). unknown names answer the zero address, never an error. signatures are valid for five minutes.

what it means for you

  • as a holder — repoint alice.fomo and alice.fomodomains.eth follows within seconds, because there is nothing to sync: the ENS answer is read live from the contract.
  • as a developergetEnsAddress({ name: 'alice.fomodomains.eth' }) in viem, provider.resolveName() in ethers, or any ENS-aware library. no fomo-specific code.
  • trust model — the gateway is the only off-chain piece. it can refuse to answer (then the name does not resolve) but it cannot lie: an answer that does not match what its key signed, or a key the resolver's owner has not authorized, is rejected by the resolver contract on Ethereum.

profiles

every name has a public page at fomodomains.com/{name}: the on-chain card, the address it points to, its ENS name, its ownership history, and a profile the owner fills in. profiles live in a second contract, FomoRecords, as ENS-style text records.

TXFomoRecords.setTexts(string name, string[] keys, string[] values)

write your profile

owner-only, checked live against FomoNames.ownerOf. keys follow the ENS convention: avatar, description, url, com.twitter, org.telegram, com.github, com.discord. one transaction saves every field. keys are at most 64 bytes, values 2048.

profiles do not travel with a sale. every record remembers the owner who wrote it; when a name changes hands (transfer or purchase) the previous owner's records are hidden and the new owner starts clean. the old records are not deleted, they are simply never returned for a different owner, so a name bought back later does not resurrect a stranger's bio.

GET/api/profile/{domain}

profile

the resolve payload plus texts (the records above, only when set), image (the on-chain SVG as a data URI), ens (name.fomodomains.eth) and history: every mint, transfer and sale of the token from the contract's events, oldest first, with block timestamps and transaction hashes. cached 30s at the edge; ?fresh=1 bypasses it.

{
  "name": "alice", "owner": "0x…", "target": "0x…", "tokenId": "…",
  "texts": { "description": "gm.", "com.twitter": "alicefomo", "url": "https://alice.example" },
  "image": "data:image/svg+xml;base64,…",
  "ens": "alice.fomodomains.eth",
  "history": [ { "kind": "registered", "to": "0x…", "txHash": "0x…", "at": "…" }, { "kind": "sold", "from": "0x…", "to": "0x…", "priceEth": "0.1", … } ]
}

safety

the website only ever renders http(s):// and ipfs:// links and images; anything else stays plain text. handles are limited to letters, digits, dots, dashes and underscores. the same records are exposed through ENS: text(node, "com.twitter") on alice.fomodomains.eth returns what alice wrote on-chain, and text(node, "url") defaults to her profile page.

primary names

forward resolution turns alice.fomo into an address. a primary name goes the other way: an address says which name it wants to be shown as, and every app that asks gets alice.fomo instead of 0x93b7…7e02. on this site it replaces addresses in the wallet button, the registry, the market and every profile.

TXFomoReverse.setPrimary(string name) · clearPrimary()

set yours

the caller must be the address the name resolves to (FomoNames.targetOf), otherwise it reverts with NotTarget. the owner of a name that points elsewhere has to repoint it to themselves first. one call, no fee.

a primary name can never lie. it is checked again on every read: if the name is repointed, transferred or sold, primaryOf returns nothing for the old address, with no transaction from anyone. point it back and it comes back. a buyer does not inherit it and has to opt in.

GET/api/reverse/{address} · /api/reverse?addresses=a,b,c

look it up

one address, or up to 100 at once for lists. only named addresses appear in the batch answer. cached 15s at the edge; ?fresh=1 bypasses it. /api/wallet/{address} also carries primary.

GET /api/reverse/0x93b7…7e02
{ "address": "0x93b7…7e02", "name": "alice", "fqdn": "alice.fomo" }

GET /api/reverse?addresses=0x93b7…,0x2b7f…
{ "names": { "0x93b7…7e02": "alice" } }
CALLprimaryOf(address) · primariesOf(address[])

or read the contract

the same answer straight from Robinhood Chain, one address or a batch. build a leaderboard, a chat, a portfolio tracker that shows names instead of hex, without asking us.

domain rules

  • character set — a-z, 0-9 and hyphens. enforced by the contract on the raw bytes: uppercase and any non-ASCII byte revert, so Unicode look-alikes cannot exist on-chain. the website lowercases what you type before sending.
  • length — 3 to 63 characters, excluding the TLD.
  • hyphens — no leading, trailing or consecutive hyphens.
  • reserved — admin, www, api, support, help, mail, root, system, official and fomo. the owner wallet can extend the list; reserving a name never affects one that is already registered.

error codes

the api answers non-2xx with a stable error.code. the contract reverts with typed custom errors, which the website decodes before you sign.

INVALID_NAME400the domain failed validation.
INVALID_WALLET400not a valid 0x address.
NOT_FOUND404the domain is not registered.
NOT_LISTED404the name is not for sale.
ONCHAIN410legacy write route: send the transaction to the contract instead.
TOO_MANY400more than 100 addresses in one reverse lookup.
RATE_LIMITED429too many requests from this IP; honor Retry-After.
RPC_ERROR502no Robinhood Chain RPC could be reached and no cached answer was available.
NOT_CONFIGURED503deployment is missing its configuration.
InvalidName()revertcharacter set, length or hyphen rule violated.
NameReserved()revertthe name is on the reserved list.
NameTaken()revertthe token already exists.
NotFound()revertno token for that name.
WrongValue(expected, sent)revertmsg.value is not exactly the required amount.
NotAuthorized()revertcaller is not the owner (or approved) of the token.
NotForSale()revertsalePrice is zero.
PriceTooLow()revertlisting below 0.001 ETH.
SelfPurchase()revertthe buyer already owns the name.
TransferFailed()revertthe seller or treasury refused the ETH; state rolled back.
MigrationIsClosed()revertadmin minting is permanently closed.
NotTarget()revertFomoReverse: the name does not point to the caller.

legacy api

before the contract, the registry ran on a database with a reserve → pay → confirm flow and signed messages for updates (POST /api/register/*, /api/update, /api/market/list|unlist|reserve|confirm). those routes now answer 410 ONCHAIN with the contract address. the 28 names registered that way were minted to their owners in a single migration transaction, keep their original registration dates, and the migration path was then closed for good.