# .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://testnet.cellula.id/api OpenAPI: https://testnet.cellula.id/api/openapi.json Human version of this page: https://testnet.cellula.id/developers ## Start here ### Resolve a name and pay it ```js const r = await fetch('https://testnet.cellula.id/api/resolve/maria.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://testnet.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, `domain` when the name claims a website, which you should show only when its `verdict` is `answers`, and `accounts`, the accounts on X, GitHub and Bluesky the name claims (decision 0040), each shown only when its own `verdict` is `answers`, with the `url` of the post, gist or file that carries the account's half. 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 - `/directory?q=&sale=&pay=&page=1&size=10` the names a page at a time, searched and filtered, in label order. From the snapshot the resolver refreshes every few seconds, so a list of thousands is one call instead of a round trip per name. Withdrawn names are left out; `exists` says whether a name spelled exactly like `q` exists on the chain, because a withdrawn name is not free. Every row carries the outpoint it was read at. 503 until the first snapshot. - `/meals/:name?limit=50` who fed the being of a name, newest first. Payments into the lock of the name’s deed that carry a meal naming it (decision 0037: the witness `cells:meal:1`, a zero byte, then `{ to, note }` as JSON). Kept by reading each block a few blocks behind the tip; `readTo` is the last block read and `behind` how far that is from the tip. A payment into the address the name publishes, a payment for another name, a name not held as a deed, and money the holder moves out of the lock are not meals. Each payer carries their own verified .cell when they have one. Returns 503 until the first blocks have been read. - `/stealth?from=22665400` every private payment to a name since a block, oldest first. Transactions carrying the stealth witness (decision 0051: `cells:stealth:1`, a zero byte, the payer’s one-time point R in 33 bytes, then a view tag in 1 byte), with their outputs under the default lock. It names nobody: a keeper’s app reads all of them and finds its own with its scan key, so every caller gets the same rows. `next` is the block the next page starts at. Kept by reading each block a few blocks behind the tip; returns 503 until the first blocks have been read. - `/dob/0xffffb305f0519f378389c5833fe4b001262e415e2336005e536555ce0f92e330` a Spore’s DOB drawn as one SVG, for an . A DOB/0 or DOB/1 Spore, decoded by a DOB decoder: a DOB/1 as the decoder composites its layers, a DOB/0 as a card of its traits over its background image. Every image is inlined (ipfs:// and btcfs:// through their public gateways); scripts and event handlers are taken out, and the answer carries a policy under which none could run. Drawn once and kept. Testnet only, where a decoder is configured; 503 elsewhere, 404 for a Spore no decoder can read. - `/token/0x1fed7ded0619c2d083d2248b80acf77161b587f6cf41098fbc348a9788f3d24d` what an xUDT or sUDT token is called, by its type hash. The symbol, name and decimals the issuer published, and the tags the explorer gives it (rgb++ among them), read from the Nervos explorer, which a page cannot ask itself. 404 when nobody named the token. A known answer is kept an hour. - `/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 a name's website and account proofs to be looked at again. At most once a minute. It queues the check rather than running one; the answer turns up on `/resolve`. - `/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 /statement` check a statement a name signed. `{signed}`, `{block}` or `{token}` (decision 0039). Says whose key signed it: the owner today, a former owner and the block it let go, the manager, or nobody. Never when it was signed; the date inside is the signer's word. The same check is `verifyStatement` in the SDK. - `GET /deed/:name` is the name held as a deed, and by whom. A name owned by a proxy lock over a Spore is held by whoever holds that Spore (decision 0036). `deed` is null for a name owned by a key; the same link is `deedOf` in the SDK. - `GET /deeds/lease` a plain cell of the deeds key for a new deed to spend. A deed may spend one plain cell of the key that holds the deeds cluster, handed back unchanged, instead of the cluster cell itself, so deeds made at the same moment do not collide. Lent to one caller for three minutes and never cached; a lease is a hint, not a lock. - `POST /deed/cosign` the deeds cluster's signature on a transaction that makes a deed. `{name, tx}`, the transaction in the node's JSON shape. Refused unless it is exactly a deed for that name (`checkDeedTx` in the SDK); the key signs the cluster cell and nothing else. - `POST /account` retired: a wallet's own account costs nothing to make since lock v6. `{pub}`, the passkey's 64-byte P-256 key. Makes the account's settings cell under the ckb-smart-account lock and pays the 420 CKB it holds; answers `{id, address, txHash, configCell}`. A few a day per address. On mainnet a wallet the person has pays (`buildAccountCreation` in the SDK). - `POST /report` report a name under the disputes policy. `{name, claim, statement, evidence, contact}`. Acknowledged within five working days. - `POST /statement/keep` hold a statement so its link can be short. As JSON `{token}`. Stored as it is, since a statement is public by nature, under twelve characters derived from it (the first nine bytes of the SHA-256 of its token), for ten years. Answers `{id, link}`. The long link, with the statement in it, works without this. - `/statement/:id` a statement held under a short name. `{token, expires}`, or 404 when nothing is held under it. - `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`, open since 2026-09-21. `/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-29: `cellula-sdk` 0.2.0 is on npm: `cellResolver`, `deploymentFor(network)` so a write needs no configuration, deeds (with `@ckb-ccc/spore` as a peer), signed statements and meals. The mainnet deployment points at the contracts upgraded on 2026-09-28. - 2026-09-28: `POST /statement/keep` holds a statement under twelve characters derived from it, and `/statement/:id` gives it back: the check page `/signed/` and its link preview work from either. The long link keeps working without the resolver. - 2026-09-27: `/resolve` carries `accounts`: accounts on X, GitHub and Bluesky a name claims under `proof.x`, `proof.github` or `proof.bluesky`, each with its `verdict`, its `why` and the `url` of the post, gist or file that carries the account's half, a statement the name's owner signed (decision 0040). A statement's link now carries it in the query, `/signed?s=`, so a shared link's preview says who signed it; links after `#` still open. - 2026-09-26: `/health` carries `namespace`: `configTypeHash`, the 32-byte namespace id a sign-in message commits to as `ns`, and `args`, its first 20 bytes, the account type script’s args. The sign-in example on the developers page pointed at `/health` for it before it was there. - 2026-09-26: `cellula-sdk` gains `cellResolver`, a `.cell` resolver for the `addressResolver` hook `@ckb-ccc/core` 1.23.0 added (ckb-devrel/ccc#575). It takes `proof.outPoint` from `/resolve` as a hint and checks it against the app’s own client before answering; a wrong or stale hint falls through to the chain. In the next release of the package. - 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://testnet.cellula.id/llms-full.txt