REST API
A read-only HTTP resolver for @handles and X1NS domains, for services that can't or won't run an RPC client.
REST API
x1id resolves both @handles and X1NS domains (.x1 / .xnt / .xen) directly from on-chain state — that's what the SDK does, and what the on-chain reference documents for anyone replicating it in another language. The REST API described here does the same reads over plain HTTP, for a service that can't or won't run an RPC client.
This API is optional infrastructure, not the source of truth. Every answer it returns is also derivable by reading chain directly, and nothing in x1id depends on this service being up. Treat an outage as "resolve locally," never as "the name is free."
It is read-only by construction: there is no write path, no key, and no mutation endpoint. The worst a compromised or malicious instance could do is lie about a resolution result — which a caller can always catch by re-checking on-chain, the same fallback the SDK documents. Because there's nothing to protect, CORS is wide open (Access-Control-Allow-Origin: *), so the API is safe to call directly from a browser or embed cross-origin.
Base URL
There is no single hosted instance this documentation points you at — run your own (see Self-hosting below), or use whichever instance your integration has been configured to talk to. Examples on this page use a local instance:
http://localhost:8691Endpoints
GET /health
Liveness check. Always answers from process state, no RPC call involved.
curl -s http://localhost:8691/health{ "ok": true }GET /version
Reports the RPC node's Solana core version and the RPC URL this instance is configured against. Requires the RPC endpoint to be reachable.
curl -s http://localhost:8691/version{ "solana_core": "1.18.22", "rpc": "https://rpc.testnet.x1.xyz" }GET /resolve/:name
Resolves a single @handle or X1NS domain. :name must be URL-encoded — in particular, @ must be sent as %40.
# @handle
curl -s http://localhost:8691/resolve/%40nike
# X1NS domain
curl -s http://localhost:8691/resolve/alice.x1@nike — 200 OK:
{
"input": "@nike",
"name": "nike",
"namespace": "handle",
"owner": "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2",
"verification": "owner"
}alice.x1 — 200 OK:
{
"input": "alice.x1",
"name": "alice.x1",
"namespace": "x1",
"owner": "6UdHWxdpC9zVV5XVvNerya7Dio3mnscgz1Np9t1Yxmuc",
"verification": "owner"
}Response fields
| Field | Type | Description |
|---|---|---|
input | string | The name exactly as decoded from the request path. |
name | string | The canonicalized name that was actually resolved. |
namespace | string | "handle" for an @handle, or the TLD ("x1", "xnt", "xen") for an X1NS domain. Always render this before letting a user act on the result — @jack and jack.x1 can resolve to different owners. |
owner | string | Base58-encoded owner address. |
verification | string | How the owner was established. Currently always "owner" — the address that controls the on-chain registry account for this name. |
A bare address is never a complete answer on its own. Always check namespace alongside owner — the API never silently picks one namespace over another, and neither should your integration.
Never guessed: mixed-shape input
A string that looks like both a handle and a domain (@jack.x1) is refused outright rather than resolved to one or the other:
curl -si http://localhost:8691/resolve/%40jack.x1 | head -1
# HTTP/1.1 409 Conflict{ "error": "ambiguous: both a handle and a domain shape", "input": "@jack.x1" }This check runs on the shape of the input alone, before any RPC call, so it answers even when the upstream chain is unreachable.
Status codes
| Code | Meaning | Example body |
|---|---|---|
200 | Resolved successfully. | {"input", "name", "namespace", "owner", "verification"} |
400 | Input isn't a valid handle or domain shape. | {"error": "invalid handle", "input": "..."} or {"error": "invalid domain", "input": "..."} |
404 | Well-formed name, not registered — or, for a domain, an account exists at the derived address but doesn't belong to the claimed TLD. | {"error": "handle not registered", "input": "..."} or {"error": "domain not registered", "input": "..."} |
409 | Input matches both the handle shape and the domain shape. | {"error": "ambiguous: both a handle and a domain shape", "input": "..."} |
502 | The account exists but is unreadable, or the upstream RPC is unreachable. | {"error": "malformed account"} or {"error": "rpc unreachable"} |
All error bodies are JSON with an error string and (except for the bare not found on unknown routes) the original input.
Requests other than GET receive 405, and a GET on any path other than /health, /version, or /resolve/:name receives a plain 404 with {"error": "not found"}.
Self-hosting
The API ships as one role in the x1id Docker package — a small Rust binary with no database and no key. It talks to a public X1 RPC endpoint and holds nothing sensitive, so it's safe to run yourself if you want a dedicated instance behind your own domain, rate limits, or caching layer.
docker compose --profile api up -d apiBy default it binds 0.0.0.0:8691 and points at https://rpc.testnet.x1.xyz. Both are configurable via environment variables:
| Variable | Default | Description |
|---|---|---|
API_BIND | 0.0.0.0:8691 | Address and port the server listens on. |
X1_RPC_URL | https://rpc.testnet.x1.xyz | RPC endpoint used to read account data. Point this at https://rpc.mainnet.x1.xyz to resolve X1NS domains against mainnet (see the network table for which namespace lives on which network). |
Once it's up:
curl -s localhost:8691/healthPut it behind whatever reverse proxy and TLS termination you'd use for any other service — the container itself only serves plain HTTP.