# .cell names on Nervos CKB > Resolve .cell names on the Nervos blockchain over HTTP. No account, no API key, no package, no blockchain code. CORS is open, so a browser page can call it directly. The chain is the source of truth and this is a cache in front of it; every answer carries a proof you can check against any CKB node. Base URL: https://cellula.id/api OpenAPI: https://cellula.id/api/openapi.json Human version of this page: https://cellula.id/developers ## Start here ### Resolve a name and pay it ```js const r = await fetch('https://cellula.id/api/resolve/cellula.cell').then(r => r.json()) if (r.registered && r.addresses.ckb) payTo(r.addresses.ckb) // r.proof carries the outpoint, type script and data hash to check this against a node ``` ### Show a name instead of a raw address ```js const { name } = await fetch('https://cellula.id/api/primary/' + address).then(r => r.json()) // name is that address's own verified .cell, or null. Checked both ways, so it cannot be spoofed. ``` ## Every route - `/resolve/:name` the name decoded: addresses, lightning, fiber, expiry, owner. Also carries `dispute` when a notice applies, `quantumOwner` when the owner is a post-quantum key, and `domain` when the name claims a website, which you should show only when its `verdict` is `answers`. Every answer carries a `proof`: outpoint, full type script, data hash, and the block that committed the cell. - `/resolve/:name?coinType=309` one address by SLIP-44 coin (0 btc, 60 eth, 309 ckb) - `/primary/:address` the address's own verified name, or null - `POST /primary` the same answer for up to fifty addresses at once. `{addresses: [...]}` in, `{results: [...]}` out, same order, each shaped like a single answer. One unreadable address carries `error` and does not fail the rest. For a list on a page, one call instead of one per row. - `/reverse/:address?coinType=309` names that publish this address as a payout - `/name/:name` the raw on-chain record set - `/names` every registered name - `/market` every name its owner has listed for sale, cheapest first. Carries the split the contract enforces on each price, so a storefront can show what the seller receives without computing it. Names withdrawn under the disputes policy are absent and a name under a notice carries it, which a scan of the chain cannot know. 503 until the first scan finishes, which is not the same as nothing being for sale. Buying without this service is docs/SALE-LOCK.md. - `/health` snapshot freshness, and the discount in force - `/price` what a coin is worth. Cached here so a browser need not ask a rate service itself. - `/verify` does the running contract match the published build?. Live, and checkable against the chain yourself. - `/.well-known/lnurlp/:name` the Lightning Address lookup, forwarded unchanged - `/.well-known/lnurlp/:name/invoice?amount=` an invoice for that many millisats, from the name’s own wallet. Forwarded and returned byte for byte: the invoice commits to that wallet’s own metadata, so nothing is re-signed here. - `/avatar/:name` the name's picture, always an image: its own, or the mark drawn from its id - `/cover/:name` the wide band across the top of its page, always an image. Its own picture, or a band drawn from its id. `?fallback=none` returns 404 instead of drawing one. - `/profile/:name` what a name says about itself, and where its picture came from, as JSON - `/archive/:name` every version of a name’s records this service has seen. Oldest first, so a change to where a name points is visible rather than silent. - `/latest?limit=20` names registered most recently, newest first - `/expiring?days=30&state=all` names running out, and the ones that are free to register now. Three states. `expiring` is still paid up; `grace` has lapsed but for thirty days more nobody else may take it; `free` is takeable. `state` filters to one, `days` bounds the future only, `format=rss` gives a feed. - `/recheck/:name` ask for that name's website proof to be looked at again, at most once a minute - `/disputes` every name under a notice or withdrawn from this service, and why - `/quantum` the owners proven to be post-quantum keys. A name missing from the list is not shown to be, never shown not to be. Also reports whether the code those names obey is still the code we recorded. - `POST /login` check a sign-in signature. `{signed, domain, nonce, allowManager?}`. You issue and spend the nonce; this service keeps none and cannot tell a fresh one from a used one. - `POST /report` report a name under the disputes policy. `{name, claim, statement, evidence, contact}`. Acknowledged within five working days. - `POST /r` hold a sealed payment request so its link can be short. The id is a hash of a key this service never sees and the blob is ciphertext under it, so nothing held here is readable to us. - `/r/:id` a sealed payment request, for whoever holds the key that names it - `/openapi.json` this same list as an OpenAPI 3.1 document, for a generator, a typed client or an agent ## Things that trip people up - A name nobody registered answers 404 with `{ registered: false }`. That is an answer, not an error. - `:name` takes `alice` or `alice.cell`. A sub-name is `shop.alice`, and only ever in that form. - The registration fee is a ceiling in CKB that one on-chain cell may discount. Read `price.factorBps` from `/health` rather than hardcoding the schedule: 10000 means no discount, 9000 means ten percent off. - The bytes a login signs are built in a fixed key order and begin with `t`. A payment request begins with `v`. Neither can be replayed as the other, and reordering the keys breaks the signature. - `/expiring` never drops a lapsed name because of the `days` window: the window bounds the future only. - The rate limit is 120 requests a minute per calling IP, not per name and not per user, then a 429. Read that twice if you call from a server: all your users share one budget. A list on a page is one `POST /primary` with up to fifty addresses, not fifty calls. Answers are cached for ten seconds; errors never are. - `/primary` reads the chain, so an answer it does not already hold takes seconds rather than milliseconds. Two caches sit in front of it and they are not the same one: `cache-control` asks your side to reuse an answer for ten seconds, and we keep our own copy of each address for a minute, so a name can be about seventy seconds old at worst and a first lookup is always the slow one. Ask it once where you already know whose address it is, keep the answer beside the address, and leave the path that draws a page alone. Give the call a timeout in seconds: a cap under the real answer time drops names in silence. - Treat every failure as no name, a 200 that is not JSON included. A host that is up but not serving the API can answer HTML with a 200, so code that branches on the status code alone reads a web page as an identity. Parse first, then check the shape. The two honest answers are a 200 with `name: null`, meaning that address has no name, and a 400 with the same `name: null` and an `error`, meaning the address itself could not be read. Show the same thing for both; only the second one says the bug is on your side. - Ask `/health` once when your service starts and log one line with the answer. `ok` proves you reached this API and not a host that answers 200 with a web page, and `network` tells you whether you are on `pudge`, the test network, where names cost nothing and belong to nobody. Treating every failure as no name is the right advice and is also what makes a broken integration silent, so this one call is what separates 'connected, nobody has a name yet' from 'nothing ever arrives'. - Do not make a `.cell` the fallback for a name your app already has. The first site to integrate counted twenty-five of its twenty-seven authors as having typed a display name, so showing the `.cell` only when that was empty would have reached two of them, and never on the byline of somebody who had paid for one. Show both, the `.cell` under the name, the way a handle does: a nickname says who somebody is, and only the `.cell` can be paid. - `/primary` takes a CKB address (`ckb1…` on mainnet, `ckt1…` on the test network) or an Ethereum `0x…` address, which is asked as the two OmniLock locks that key owns CKB under, the same two CCC's EVM signer derives, so an EVM app needs no CKB knowledge. A Bitcoin address, or a key behind a passkey lock, cannot be derived from its address and is a 400; for those, `/reverse/:address?coinType=0` lists names that publish the address as a payout, unverified by nature, a hint and never an identity. - A name is lowercase a to z, digits and hyphens inside a part, at most forty characters before `.cell`, with at most one dot: `shop.alice.cell` is a sub-name and `a.b.c.cell` cannot exist. Check what a resolver returns against that before storing or rendering it; it is somebody else's service and its answer lands on your page. - `/primary` says when a name runs out (`expiredAt`, `expires`, `expired`, the same fields as `/resolve`) and keeps returning an expired name, flagged, until somebody else registers it. What it cannot say is whether the name is still theirs next month: a name can be sold or handed on while the address underneath never moves. Keep the address as your key and the name as a label on it, and ask again whenever you already know who you are talking to, usually at sign-in. - Two networks, one API. The test network answers at `https://testnet.cellula.id/api`, where a name costs nothing; mainnet answers the same routes at `https://cellula.id/api` once registration opens there. `/health` returns `network` (`pudge` is the test network) so a service can tell which one it is pointed at. - Paying in USDC is an ordinary ERC-20 `transfer` to `addresses.eth`; nothing here holds or forwards it. When it settles a signed request, the request's fingerprint is appended to the transfer's calldata, which is what lets a transaction be matched to an order without trusting whoever claimed it. Paying in Bitcoin is a plain transfer to `addresses.btc`, built in the browser and signed by the payer's own wallet, and no request travels with it. ## What changed - 2026-09-23: The service behind this API is open source at github.com/LusoCryptoLabs/cells-resolver, the copy that runs here, and `docker run -e CKB_NETWORK=mainnet -p 8787:8787 cells-resolver` is a complete second instance. The contracts it reads are public at github.com/LusoCryptoLabs/cells-contracts with byte-for-byte reproducible builds. - 2026-09-19: `addresses` carries `btc` and `eth` where the owner published them, and a signed request may name `usdc-base`, `usdc-eth` or `usdc-arb` as its `unit`. No route changed: this is what the existing fields now carry. - 2026-09-17: `/primary` says when the name runs out: `expiredAt`, `expires`, `expired`, the same three fields as `/resolve`. An expired name is still returned, flagged, until somebody else registers it. - 2026-09-17: `/primary/0x…` accepts an Ethereum address and asks as the OmniLock locks that key owns CKB under. It used to answer 400. - 2026-09-17: `POST /primary` answers up to fifty addresses in one call, in order, one unreadable address not failing the rest. - 2026-09-16: `llms.txt` and `llms-full.txt` are written at build time from the same list as this page and the OpenAPI document, and a test fails if the three disagree. The fuller version, including the on-chain layout so this can be reimplemented against any CKB node without us: https://cellula.id/llms-full.txt