A shielded name service for Zcash.
Indexer · Protocol · Try it · Claim a name · Self-hosting · OpenRPC
ZNS maps human-readable names like alice.zcash to Zcash shielded addresses. Each protocol action (claim, list, buy, etc.) is an Ed25519-signed memo inside an Orchard note, authorized by a single admin key held by the registry operator. An indexer scans the chain, verifies every signature, and exposes a JSON-RPC API; any client can re-verify the indexer's responses against the admin pubkey it publishes via its status method.
The indexer turns the chain into a name service:
- Streams blocks from lightwalletd
- Decodes ZNS memos out of shielded Orchard payments
- Verifies the admin signature on each one
- Writes the result to a local SQLite database
- Serves it over JSON-RPC
State is a pure function of (chain, viewing key, admin pubkey), so two indexers given the same inputs converge on the same database. Don't trust the public one? Run your own.
ZNS lives in the 512-byte Orchard memo field as a colon-delimited UTF-8 string. The : byte is the only delimiter and is forbidden inside fields.
| Action | Memo | Accepted if |
|---|---|---|
| Claim | ZNS:CLAIM:<name>:<ua>:<sig>[:<user_pubkey>] |
name free, payment ≥ tier cost |
| List | ZNS:LIST:<name>:<price>:<nonce>:<sig>[:<user_pubkey>] |
name owned, nonce strictly increasing |
| Delist | ZNS:DELIST:<name>:<nonce>:<sig>[:<user_pubkey>] |
name listed, nonce strictly increasing |
| Release | ZNS:RELEASE:<name>:<nonce>:<sig>[:<user_pubkey>] |
name owned, nonce strictly increasing |
| Update | ZNS:UPDATE:<name>:<new_ua>:<nonce>:<sig>[:<user_pubkey>] |
name owned, nonce strictly increasing |
| Buy | ZNS:BUY:<name>:<buyer_ua>:<sig>[:<user_pubkey>] |
name listed, payment ≥ listing price |
[:<user_pubkey>] is optional. When present, <sig> must be signed by that key instead of the admin key, and all future actions on the name must be signed by the same key. This is sovereign ownership - the name is controlled by the user's key, not the registry operator's.
<sig> is base64 Ed25519 over everything between ZNS: and :<sig>:
CLAIM:<name>:<ua>
UPDATE:<name>:<new_ua>:<nonce>
BUY:<name>:<buyer_ua>
LIST:<name>:<price>:<nonce>
DELIST:<name>:<nonce>
RELEASE:<name>:<nonce>
To verify, reconstruct one of these byte-for-byte from the API response. If the response includes a non-null pubkey field, Ed25519-verify against that key; otherwise verify against status.admin_pubkey.
Field encoding:
- Names:
[a-z0-9]{1,62} - Numbers: decimal ASCII, no leading zeros
- Addresses: exact bech32m, verbatim
- Nonces: per-name, strictly increasing, reset to 0 on
BUY
Reference implementation: src/memo.rs.
Shorter names cost more zatoshis to claim; current tiers live in status.pricing.tiers (indexed by length, last entry catchall, 0 free).
ZcashMe runs a public read-only deployment:
| Network | URL |
|---|---|
| Testnet | https://light.zcash.me/zns-testnet |
| Mainnet | https://light.zcash.me/zns-mainnet-test |
The admin pubkey for both is ce86eb1b2030a4cde6b42d15a3850e9346dcf58820d20743783f1d09000e5c8e, also returned as status.admin_pubkey. Examples below are curls you can paste straight into a terminal, showing only the result field (full schema in openrpc.json).
Who is alice? Pass an empty string as the query to list all registrations, paginated with limit/offset just like the listings method.
# Exact name lookup (returns object or null)
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"resolve","params":["alice"]}'
# List all registrations (first 50 results)
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"resolve","params":["", 50, 0]}'
# List all registrations (next page)
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"resolve","params":["", 50, 50]}'
# Query by address to get all names for that address (paginated)
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"resolve","params":["u1qqlzrf9...", 100, 0]}'Single name query returns an object or null:
{
"name": "alice",
"address": "u1qq...fff",
"height": 3901200,
"nonce": 2,
"last_action": "UPDATE",
"signature": "AQID...",
"pubkey": null,
"listing": null
}Listing all registrations or querying by address returns an array:
[
{
"name": "alice",
"address": "u1qq...fff",
"height": 3901200,
...
},
{
"name": "bob",
"address": "u1qq...aaa",
"height": 3901100,
...
}
]- Empty query (
"") withlimit/offsetlists all registered names, useful for explorers or browsers. - Exact name query returns single object or
nullif not found. - Address query returns array of all names registered to that address (paginated).
- If a name is listed for sale,
listingcontains details (price,nonce,signature). pubkeyisnullfor admin-signed names; for sovereign names it's the owner's Ed25519 public key.- Pagination follows the same
limit/offsetpattern as thelistingsandeventsendpoints.
What's on the market? Use limit and offset to paginate – defaults are limit=50, offset=0, and limit caps at 500.
# First 50 listings (default)
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"listings","params":[]}'
# Next page (offset 50)
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"listings","params":[50, 50]}'{
"listings": [
{ "name": "bob", "price": 100000000, "nonce": 3, "height": 3901300, "signature": "..." },
{ "name": "carol", "price": 50000000, "nonce": 1, "height": 3901250, "signature": "..." }
],
"total": 2
}Prices in zatoshis (1 ZEC = 100M zats): bob wants a full ZEC, carol half.
Sync height, admin pubkey, current pricing. The first call any client makes.
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"status"}'{
"synced_height": 3902500,
"admin_pubkey": "ce86eb1b2030a4cde6b42d15a3850e9346dcf58820d20743783f1d09000e5c8e",
"uivk": "uivk1...",
"registered": 42,
"listed": 3,
"pricing": {
"nonce": 1,
"height": 3901000,
"tiers": [500000000, 100000000, 50000000, 10000000, 5000000]
}
}The full activity log. Filter by name, action, or since_height; paginate with limit / offset. Every event carries its signature, so the log is auditable years later.
# Filter by name
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"events","params":{"name":"alice"}}'
# Paginate results (first 50 events, starting from position 0)
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"events","params":{"name":"alice","limit":50,"offset":0}}'
# Next page (events 51-100)
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"events","params":{"name":"alice","limit":50,"offset":50}}'
# Get all CLAIM actions
curl -s https://light.zcash.me/zns-mainnet-test \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"events","params":{"action":"CLAIM","limit":100}}'{
"events": [
{ "id": 45, "name": "alice", "action": "LIST", "txid": "def...", "height": 3901300, "ua": "u1qq...fff", "price": 50000000, "nonce": 1, "signature": "..." },
{ "id": 12, "name": "alice", "action": "CLAIM", "txid": "abc...", "height": 3901200, "ua": "u1qq...fff", "price": null, "nonce": null, "signature": "..." }
],
"total": 2
}alice's whole story: claimed at block 3,901,200, listed half a ZEC later.
Pagination parameters:
limit: Maximum number of results (default 50, max 500)offset: Number of results to skip (default 0)total: Total count of matching events (for UI pagination)
All three endpoints (resolve, listings, events) follow the same pagination pattern.
To claim a name on the public ZcashMe instance, sign with your own Ed25519 key - no admin involvement required. The key can be derived deterministically from your Zcash seed phrase using SLIP-0010, so no separate key management is needed.
import { claimPayload, buildClaimMemo } from "zns-sdk";
const payload = claimPayload("alice", "u1...");
const signature = sign(payload); // Ed25519 sign with your key
const userPubkey = getPublicKey(); // base64-encoded Ed25519 public key
const memo = buildClaimMemo("alice", "u1...", signature, userPubkey);Once claimed, all subsequent actions on the name must be signed by the same key. The ZcashMe admin key is used only for protocol-level operations (pricing tiers, etc.) - it cannot authorize actions on names you own.
The indexer is deterministic. Two instances given the same UIVK and admin pubkey converge on the same database.
The admin key authorizes SETPRICE and any admin-signed actions. Generate a fresh Ed25519 keypair - any standard tool works. The indexer needs the hex-encoded 32-byte public key.
# example using openssl
openssl genpkey -algorithm ed25519 -out admin.pem
openssl pkey -in admin.pem -pubout -out admin_pub.pemThe indexer needs a Unified Incoming Viewing Key to decrypt incoming Orchard notes. Create a Zcash wallet, export the UFVK, then extract the UIVK:
cargo run --manifest-path tools/ufvk-to-uivk/Cargo.toml -- mainnet <your-ufvk>docker build --build-arg FEATURES=mainnet -t zns-indexer .
docker run -d \
-e ZNS_UIVK=uivk1... \
-e ZNS_ADMIN_PUBKEY=<your-hex-pubkey> \
-p 3000:3000 \
zns-indexerSubstitute FEATURES=testnet for testnet. Everything else (network, lightwalletd URL, birthday, SQLite path) bakes in at build time via src/config.rs.