FortiBlox LogoFortiBlox Docs
x1id

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:8691

Endpoints

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

FieldTypeDescription
inputstringThe name exactly as decoded from the request path.
namestringThe canonicalized name that was actually resolved.
namespacestring"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.
ownerstringBase58-encoded owner address.
verificationstringHow 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

CodeMeaningExample body
200Resolved successfully.{"input", "name", "namespace", "owner", "verification"}
400Input isn't a valid handle or domain shape.{"error": "invalid handle", "input": "..."} or {"error": "invalid domain", "input": "..."}
404Well-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": "..."}
409Input matches both the handle shape and the domain shape.{"error": "ambiguous: both a handle and a domain shape", "input": "..."}
502The 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 api

By default it binds 0.0.0.0:8691 and points at https://rpc.testnet.x1.xyz. Both are configurable via environment variables:

VariableDefaultDescription
API_BIND0.0.0.0:8691Address and port the server listens on.
X1_RPC_URLhttps://rpc.testnet.x1.xyzRPC 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/health

Put it behind whatever reverse proxy and TLS termination you'd use for any other service — the container itself only serves plain HTTP.