# .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. ``` ## From an AI agent (MCP) `cellula-mcp` is a Model Context Protocol server: an agent in Claude, or any client that speaks MCP, looks names up and prepares registrations with it. It reads the chain itself through `cellula-sdk` and holds no key: an agent that pays with its own wallet pays to the address it returns, and a registration is signed by the person, at the link it returns. On the test network, with the Cellula agent running beside it, it also pays over Fiber from that agent's own balance, within the limits its person set. ```sh claude mcp add cellula npx cellula-mcp@0.4.1 ``` Or in a client's config: ```json { "mcpServers": { "cellula": { "command": "npx", "args": ["-y", "cellula-mcp@0.4.1"] } } } ``` - `resolve_cell_name` where a name is paid: its CKB address and, when published, Bitcoin, EVM, a Lightning Address and a Fiber node; its state and expiry. Only an address written by whoever holds the name now is given; one a former owner left is listed under withheld (since 0.3.0) - `name_for_address` the .cell name a CKB address chose for itself, checked both ways - `check_cell_name` whether a name is free, with today's yearly fee and the deposit; when a taken one expires - `cell_name_registration_link` a link that opens the registration of a free name for the person to sign, with an invitation when invited_by names a registered name (a tenth of the fee to its owner, the same price for the person) - `verify_cell_statement` whether a statement was signed by whoever owns the name now., a Cellula wallet's passkey signature included (since 0.3.0) - `fiber_agent_status` testnet, with the Cellula agent: the agent's own Fiber balance, what it spent, its limits and what is left, and its address to top it up - `fiber_fetch_paid` testnet, with the Cellula agent: calls a URL and, when the seller answers HTTP 402 asking a Fiber payment (x402, scheme exact on fiber:testnet), pays it once within `max_ckb` and the person's limits, then asks again with the receipt - `fiber_pay_invoice` testnet, with the Cellula agent: pays a Fiber invoice that names its amount, and returns its receipt (the preimage) - `fiber_payments` testnet, with the Cellula agent: the recent payments and their receipts - `fiber_take_out` testnet, with the Cellula agent: closes the balance and sends it to the address the person set when starting the agent; it takes no address ## Packages - `cellula-sdk`: read and write names from an app (register, renew, transfer, sell, sign in). `npm i cellula-sdk @ckb-ccc/core`. - `cellula-id`: reading only, no keys, no dependencies. `npm i cellula-id`. Until 1.0 it reads the test network unless told otherwise: `configure({ network: 'mainnet' })`. - `cellula-mcp`: names for AI agents (see above). It reads mainnet unless `CELLULA_NETWORK=testnet`. ## Cellula Checkout and the Cellula wallet, on a site (test network) - Cellula Checkout, the Pay with Fiber button: ``, then ``, or `payWithFiber({ invoice })` / `payWithFiber({ name, amount })` from a click. The button gets `fiber-paid` with `{ payment_hash, preimage }`, or `fiber-failed`. The answer is a hint: count the payment when the invoice settles on your own node. A button drawn after the script ran is not wired. - The Cellula wallet as a `ccc.Signer`: `new CellulaWalletSigner(client)` from `cellula-sdk`; `connect()` gives the wallet's addresses, then `signMessage` and `sendTransaction` open the wallet, which shows what it signs. On Android the Cellula app answers. Try it: https://testnet.cellula.id/try-the-wallet ## 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 the sale lock itself: contracts/sale-lock in github.com/LusoCryptoLabs/cells-contracts, and decisions 0012 and 0025. - `/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, the tags the explorer gives it (rgb++ among them) and its picture, read from the Nervos explorer, which a page cannot ask itself. Add `?type=code_hash:hash_type:args`, the token’s script, and a token the explorer has no record of is named from the chain: the unique cell its owner published in the token’s first transaction, as RGB++ issuers do, or else the newest one under the owner’s lock from a transaction the owner signed that carries the token. null, with 200, 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. Answers 503 since 2026-10-08. It made a passkey's account on testnet, a settings cell under the ckb-smart-account lock, and paid the 420 CKB the cell held; since lock v6 a wallet's starter address is the passkey alone, so there is nothing to pay for. - `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 - `/.well-known/nostr.json?name=maria` NIP-05: the Nostr key a name publishes, for a Nostr client to check. CORS open and no redirect, as NIP-05 asks. A name with no key gets an empty `names`. - `maria./.well-known/atproto-did` the Bluesky DID a name publishes, as plain text, asked at the name's own host. This is how Bluesky checks a domain handle, so the name can be one. 404 when it publishes none. - `/counts` how many people reached each step of claiming a name, per day; totals only. `{days, total, steps, wallets}`; `wallets` counts first addresses made and wallets given guardians, read from the chain. - `POST /step?s=landing` count one step, from the fixed list in /counts. Anything not on the list is dropped without an error, and nothing about the caller is kept. Answers 204. - `/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 at `https://cellula.id/api`, open since 2026-09-21. Mainnet runs the last release that was promoted, so a route added on the test network since (`/token` and `/stealth` among them, as of 2026-10-30) answers 404 there until the next promotion. `/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-10-30: On npm: `cellula-mcp` 0.3.0, whose `resolve_cell_name` gives only the addresses written by whoever holds the name now and lists the rest under `withheld`, and whose `verify_cell_statement` checks a Cellula wallet's passkey signature; and `cellula-sdk` 0.3.6, which marks an invited registration for LUCEL's referral. - 2026-10-30: A name that publishes a Bitcoin address also answers BIP-353: a DNSSEC-signed TXT record at `.user._bitcoin-payment.cellula.id` (on the test network, `.user._bitcoin-payment.testnet.cellula.id`) holding a BIP-21 URI, so `₿name@cellula.id` pays from wallets that read it. It follows the chain within ten minutes. A name that publishes a Lightning address without a Lightning offer gets no record, since a wallet that tries BIP-353 first would find no Lightning there. The Lightning offer goes in as `lno=`. - 2026-10-29: `/token/:typeHash` takes `type`, the token’s script, and with it names a token the Nervos explorer has no record of from what its owner published on chain. The testnet explorer reads xUDT by type only, so a token in BEAF’s shape had no name there. - 2026-10-26: Four routes that were already served are listed here and in `/openapi.json`: `/.well-known/nostr.json`, `/.well-known/atproto-did`, `/counts` and `POST /step`. `/meals/:name` documents `offset`. - 2026-10-25: `/token/:typeHash` carries `icon`, the token's picture as a data URI, when the Nervos explorer has one and does not tag the token suspicious or invalid. - 2026-10-12: `/counts` carries `wallets`: how many first addresses and guarded wallets the chain shows, counted from blocks, with nothing about anybody. - 2026-10-08: `/token/:typeHash` says what an xUDT or sUDT token is called, and `/dob/:sporeId` draws a Spore's DOB as one SVG where a decoder is configured (testnet). `POST /account`, opened on testnet the day before, is retired and answers 503: since lock v6 a wallet's own account costs nothing to make. - 2026-10-07: `/stealth?from=` lists every private payment to a name since a block, for a keeper's app to find its own with its scan key (decision 0051). - 2026-10-02: `/meals/:name` takes `offset` for the next page. A name answers NIP-05 at `/.well-known/nostr.json?name=` and Bluesky's handle check at `/.well-known/atproto-did` on its own host. - 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 package since 0.2.0. - 2026-09-23: The service behind this API was published at github.com/LusoCryptoLabs/cells-resolver, and a clone of it runs as a second instance: `docker build -t cells-resolver .` in the clone, then `docker run -e CKB_NETWORK=mainnet -p 8787:8787 cells-resolver`. That copy was last updated on 2026-09-28, so it lacks the routes added since. There is no published image. 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` (`usdc-op` and `usdc-poly` came later). 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