A single xe binary serves everything: the block lattice, the HTTP API, the operator listener and the web UI (embedded via //go:embed, enabled with --ui). The repository's deploy/ directory and docs/run-a-node.md are the canonical operator references; this page summarises them and adds what is specific to the public testnet hosts.
Listeners
| Listener | Default | Flags | Serves |
|---|---|---|---|
| HTTP API | 127.0.0.1:8080 | --api, --api-port, --api-bind | REST + SSE API, GET / manifest |
| Web UI | 127.0.0.1:8000 | --ui, --ui-port, --ui-bind, --wallet, --ui-faucet | explorer, wallet, DAO console; reverse-proxies /api/* to the API |
| Operator | 127.0.0.1:9095 | --metrics-addr (empty disables) | /metrics (Prometheus), /health, /ready |
| libp2p | 0.0.0.0:9000 | --port | gossip, sync, netcheck, tunnel |
| SSH gateway | off | --ssh-port | SSH into leased VMs (provider mode) |
Only the libp2p port needs to be reachable from the internet. Everything else binds to loopback by default, and exposing it publicly is an explicit choice that should go through a reverse proxy. IPv6 is not supported — the node listens on /ip4/0.0.0.0/tcp/<port> only.
Embedded UI (Recommended)
xe node --ui \
--genesis-dir /etc/xe/genesis \
--dial /ip4/45.77.226.208/tcp/9000/p2p/12D3KooW... \
--api-bind 0.0.0.0 \
--ssh-port 2222(Replace 12D3KooW... with a current peer ID — see Bootstrap Peers — and put the published ledger-genesis.json and statechain-genesis.json for the network in the genesis directory.)
[!WARNING] Provider mode requires KVM
--provideis deliberately absent from the example above — it is opt-in, and the live bootstrap nodes do not run it. It enables QEMU/Lima VM provisioning, which hard-requires KVM (/dev/kvm) on the host, and a provider node auto-accepts matching leases as soon as the flag is set. Only add--provideon bare metal or a VPS with nested virtualization enabled, and set the provider policy flags to bound what it will accept.
Packaging
| Artifact | Path | Notes |
|---|---|---|
| systemd unit | deploy/xe-node.service (+ deploy/systemd/) | Hardened: ProtectSystem=strict, ReadWritePaths=/var/lib/xe; reads /etc/xe/xe-node.env (deploy/xe-node.env.example lists every knob). Providers add a kvm.conf drop-in with SupplementaryGroups=kvm |
| Docker | deploy/Dockerfile | Static, non-root, distroless image; mount the genesis at /genesis and pass --genesis-dir /genesis |
| pm2 | deploy/pm2/ecosystem.config.js | What the public bootstrap hosts use, behind Caddy |
| Monitoring | deploy/monitoring/ | Prometheus scrape config, alert rules, Alertmanager, Grafana dashboard, compose file — all against the operator listener |
Builds are reproducible (make build, make dist, make verify-repro; pinned toolchain in .go-version, -trimpath, -buildvcs=false, CGO_ENABLED=0). No release has been tagged yet, so docs/run-a-node.md's download-and-verify recipe (SHA256SUMS, Sigstore, gh attestation verify) describes the intended flow; build from source today.
Public Hosts (Caddy + pm2)
On the three bootstrap hosts, Caddy terminates TLS and proxies to the node's two listeners; both processes are managed by pm2.
┌─────────────┐
│ Caddy │ :80/:443 (TLS termination)
└──────┬──────┘
┌───────┴────────┐
▼ ▼
xe-node UI server xe-node HTTP API
127.0.0.1:8000 127.0.0.1:8080
(--ui, --ui-port) (--api-port)
│ ▲
└─ /api/* proxy ─┘*.core.test.network is the API listener; ldn.test.network is the UI listener (wallet and explorer). The explorer and wallet are not separate applications and there are no static UI assets to deploy. Because external clients behind Caddy share an egress bucket in the node's per-IP rate limiter, a plain sequential sweep of the public URLs can trip 429 — back off rather than retry hot.
Data Directory
| Path | Content |
|---|---|
ledger/ | BadgerDB database |
host.key | libp2p identity (peer ID) |
node.key | Node account key |
ssh_host_key | SSH gateway host key |
lima/ | Lima VM state |
lima-templates/ | Lima YAML templates |
images/ | VM base images |
The database lives in ledger/ — wiping any other directory does not clear the chain state. A data directory is bound to one network: after a testnet wipe, start with a fresh --data directory alongside the new genesis bundle, or the node stops with genesis mismatch: this data dir belongs to a different network.
Bootstrap Peers
| Node | Location | IP | API |
|---|---|---|---|
| ldn | London | 45.77.226.208 | https://ldn.core.test.network |
| ffm | Frankfurt | 192.248.176.245 | https://ffm.core.test.network |
| nyc | New York | 144.202.4.117 | https://nyc.core.test.network |
Dial multiaddrs take the form /ip4/<ip>/tcp/9000/p2p/<peer-id>; the current set is listed in Getting Started. Peer IDs are derived from each node's host.key and change if that key is regenerated, so confirm them against the API:
curl -s https://ldn.core.test.network/node | jq -r .id
curl -s https://ffm.core.test.network/node | jq -r .id
curl -s https://nyc.core.test.network/node | jq -r .idProviders
Providers participate over libp2p only; they have no public DNS or HTTP API of their own, and they advertise themselves on the xe/marketplace gossip topic. Query the live list rather than trusting any static table:
curl -s https://ldn.core.test.network/providers | jqTwo providers are registered at the time of writing (4 vCPU / 8 GB / 50 GB each, up to 5 concurrent leases). Requirements for running one: Ubuntu 24.04 or similar, /dev/kvm, QEMU and Lima (--limactl-path if not on PATH), a reachable libp2p port, and a valid performance certificate — the node benchmarks itself on startup and GET /node → certificate.valid must be true before it can be leased from. Advertise 60–70% of physical resources.
Health, Readiness and Metrics
/health (liveness: process up, sampler running, store readable) and /ready (synced, peered, delegated weight present, quorum reachable, finality advancing) are served on both the API and the operator listener; /metrics only on the operator listener. Wire supervisors to /health only — during a network-wide quorum incident every node reports not-ready, and restarting them all makes it worse. Load balancers can drain on /ready. xe_delegated_weight_micro_xe at zero and xe_finality_advances_total flat are the two alerts that matter.
Release Process
Development happens in a private repository; the public tree is its published release, cut per network (release: testnet-0005 …). Deploys to the bootstrap hosts are gated on the binary's genesis matching the live network's network_id, and protocol changes ship as a testnet wipe: new genesis bundle under genesis/<network-id>/, fresh data directories, new network ID. The documentation sites are separate Next.js applications with their own deploy workflows.