This guide covers running XPR Network nodes, becoming a Block Producer, and operating infrastructure.
XPR Network runs on Antelope/Leap (formerly EOSIO). Node types include:
| Type | Purpose | Requires |
|---|---|---|
| API Node | Serve RPC requests | Public endpoints |
| Block Producer | Validate transactions, produce blocks | Registration, KYC |
| State History | Stream full history | Large storage |
| Seed Node | P2P network connectivity | Stable connection |
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 4 cores | 8+ cores (high single-thread) |
| RAM | 16 GB | 32+ GB |
| Storage | 500 GB SSD | 1+ TB NVMe |
| Network | 100 Mbps | 1 Gbps |
- OS: Ubuntu 22.04 LTS
- nodeos: Leap 5.0.3 (the final Leap release, Oct 2024) or its successor line Spring 1.2.x — match
server_version_stringreported by mainnet APIs (v5.0.0–v5.0.3 as of Sept 2026)
Verify OS version:
lsb_release -a# Download Leap 5.0.3
wget https://github.com/AntelopeIO/leap/releases/download/v5.0.3/leap_5.0.3_amd64.deb
# Install
sudo apt install ./leap_5.0.3_amd64.deb
# Verify installation
nodeos --version
cleos --version/opt/XPRMainNet/
├── config.ini # Node configuration
├── genesis.json # Genesis block
├── start.sh # Startup script
├── stop.sh # Shutdown script
├── stderr.txt # Log output
└── data/
├── blocks/ # Block data
└── state/ # Chain statemkdir -p /opt/XPRMainNet
cd /opt/XPRMainNet
git clone https://github.com/XPRNetwork/xpr.start.git ./Edit config.ini:
# Network identity
agent-name = "MyNodeName"
p2p-server-address = YOUR_EXTERNAL_IP:9876
# Chain database
chain-state-db-size-mb = 16384
# SECURITY: Limit connections per host to prevent connection exhaustion attacks
p2p-max-nodes-per-host = 2
max-clients = 100
# Plugins
plugin = eosio::chain_api_plugin
plugin = eosio::http_plugin
plugin = eosio::net_plugin
plugin = eosio::net_api_plugin
# HTTP settings
# Bind to loopback only — nginx fronts the HTTP API (see Reverse Proxy below)
http-server-address = 127.0.0.1:8888
access-control-allow-origin = *
# Billing
disable-subjective-p2p-billing = falsep2p-peer-address = api.protonnz.com:9876
p2p-peer-address = proton.protonuk.io:9876
p2p-peer-address = proton.p2p.eosusa.io:9879
p2p-peer-address = proton.cryptolions.io:9876
p2p-peer-address = protonp2p.eoscafeblock.com:9130
p2p-peer-address = p2p.alvosec.com:9876
p2p-peer-address = p2p.totalproton.tech:9831
p2p-peer-address = mainnet.brotonbp.com:9876
p2p-peer-address = proton.eu.eosamsterdam.net:9103
p2p-peer-address = protonp2p.blocksindia.com:9876
p2p-peer-address = p2p-protonmain.saltant.io:9876
p2p-peer-address = protonp2p.ledgerwise.io:23877
p2p-peer-address = proton-seed.eosiomadrid.io:9876
p2p-peer-address = proton.genereos.io:9876
p2p-peer-address = p2p-proton.eosarabia.net:9876
p2p-peer-address = p2p.luminaryvisn.com:9876# Allow SSH FIRST — enabling ufw without this rule drops your SSH session
sudo ufw allow OpenSSH # or: sudo ufw allow 22/tcp
sudo ufw allow 9876/tcp # P2P
sudo ufw enableDon't open 8888 directly. The HTTP API stays bound to 127.0.0.1 and is reached through the nginx proxy described later in this file (port 443 only).
First run (sync from genesis):
./start.sh --delete-all-blocks --genesis-json genesis.jsonSubsequent runs:
./start.sh# Watch logs
tail -f stderr.txt
# Check sync status
cleos get info
# Verify head block
curl http://localhost:8888/v1/chain/get_info | jq '.head_block_num'mkdir -p /opt/XPRTestNet
cd /opt/XPRTestNet
git clone https://github.com/XPRNetwork/xpr-testnet.start.git ./71ee83bcf52142d61019d95f9cc5427ba6a0d7ff8accd9e2088ae2abeaf3d3dd
p2p-peer-address = p2p-protontest.saltant.io:9879
p2p-peer-address = testnet-p2p.alvosec.com:9878
p2p-peer-address = proton-seed-testnet.eosiomadrid.io:9877
p2p-peer-address = proton.testnet.protonuk.io:9876
p2p-peer-address = p2p.testnet.totalproton.tech:9842
p2p-peer-address = testnet.brotonbp.com:9877
p2p-peer-address = p2p-xpr-test.danemarkbp.com:9876
p2p-peer-address = tn1.protonnz.com:9876
p2p-peer-address = xpr-testnet-p2p.bloxprod.io:9875
p2p-peer-address = peer-xprtest.blocksforge.com:9876
p2p-peer-address = test.proton.p2p.eosusa.io:19879
p2p-peer-address = p2p.proton-testnet.genereos.io:9876
p2p-peer-address = protontest.eu.eosamsterdam.net:9905
p2p-peer-address = proton-testnet.cryptolions.io:9874
p2p-peer-address = testnet-p2p.xprlabs.org:9870
p2p-peer-address = p2p-testnet-proton.eosarabia.net:9876| Resource | URL |
|---|---|
| Explorer | https://testnet.explorer.xprnetwork.org |
| Account Creation | https://testnet.webauth.com |
| Faucet | https://testnet.resources.xprnetwork.org/faucet |
- Business Entity - Must operate as a registered business
- KYC Verification - Complete identity verification at https://identity.metallicus.com
- Testnet Performance - 14 consecutive days of block signing without interruption
- Performance Score - Achieve < 0.35ms block production time
- Code of Conduct - Sign the BP Code of Conduct
- Ownership Disclosure - Disclose beneficial ownership
Create a dedicated key pair for block signing (separate from account keys):
# Create wallet — keep the password file outside the node directory
cleos wallet create --file ~/.wallet_pass.txt
chmod 600 ~/.wallet_pass.txt
# Generate signing key
cleos create key --to-console
# Save both public and private keys securelycleos system regproducer YOURACCOUNT SIGNING_PUBLIC_KEY "https://yourwebsite.com" LOCATION -p YOURACCOUNT
# LOCATION = ISO 3166-1 numeric country code
# e.g., 840 = United States, 826 = United KingdomAdd to config.ini:
# Block producer settings
producer-name = youraccount
# Plaintext private key on disk: run `chmod 600 config.ini` and never commit it.
# Safer: keep the key in keosd and use `signature-provider = PUB_KEY=KEOSD:http://127.0.0.1:8900`
signature-provider = PUB_KEY=KEY:PRIV_KEY
# Producer plugin
plugin = eosio::producer_plugin
plugin = eosio::producer_api_plugin
# Performance
max-transaction-time = 150
abi-serializer-max-time-ms = 2000# Mandatory on a BP: producer_api_plugin is enabled above, so the HTTP API must
# never be reachable off-box. Bind to loopback — a firewall rule is not a substitute.
http-server-address = 127.0.0.1:8888Set CPU governor to performance mode:
# Check current governor
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
# Set to performance
echo "performance" | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# Make persistent (add to /etc/rc.local or systemd service)For public API nodes serving dApps:
# API plugins
plugin = eosio::chain_api_plugin
plugin = eosio::http_plugin
plugin = eosio::net_plugin
# Enable state history for Hyperion
plugin = eosio::state_history_plugin
state-history-endpoint = 127.0.0.1:8080 # NEVER expose SHIP publicly
trace-history = true
chain-state-history = true
# HTTP settings
# Bind to loopback only — nginx fronts the HTTP API (see Reverse Proxy below)
http-server-address = 127.0.0.1:8888
http-max-response-time-ms = 100
http-validate-host = false # safe only because nginx sets Host
access-control-allow-origin = *
access-control-allow-headers = *
# Increase limits for API
max-clients = 150
abi-serializer-max-time-ms = 2000upstream nodeos {
server 127.0.0.1:8888;
}
server {
listen 443 ssl http2;
server_name api.yournode.com;
ssl_certificate /etc/letsencrypt/live/api.yournode.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.yournode.com/privkey.pem;
location / {
proxy_pass http://nodeos;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Rate limiting
limit_req zone=api burst=20 nodelay;
}
}For running a full history node with Hyperion:
plugin = eosio::state_history_plugin
state-history-endpoint = 127.0.0.1:8080 # NEVER expose SHIP publicly
trace-history = true
chain-state-history = trueA remote indexer reaches SHIP over a private network interface or an SSH tunnel — never over the public internet.
See: https://hyperion.docs.eosrio.io/
Key components:
- Elasticsearch (data storage)
- RabbitMQ (message queue)
- Redis (caching)
- Hyperion Indexer
- Hyperion API
Instead of syncing from genesis (which takes days), use a recent snapshot. Public snapshot mirrors come and go — operators run them as a courtesy and URLs change without notice. Don't hard-code a mirror in production tooling; ask in the XPR Network validators group on Telegram for a currently-live mirror, or generate your own from a known-good node.
The commands below assume a .bin.zst snapshot at $SNAPSHOT_URL:
# 1) Find a currently-live mirror (ask in the XPR Network validators
# group on Telegram: https://t.me/XPRNetwork/935112 — or check your peer list).
SNAPSHOT_URL="https://<operator-provided-mirror>/latest-snapshot.bin.zst"
# 2) Download
wget "$SNAPSHOT_URL" -O latest-snapshot.bin.zst
# 3) Decompress
zstd -d latest-snapshot.bin.zst
# 4) Start nodeos from the snapshot
nodeos --snapshot latest-snapshot.binA snapshot is unsigned state from an untrusted mirror — check it before trusting the node:
# 1) Chain id must be mainnet's
curl -s http://127.0.0.1:8888/v1/chain/get_info | jq -r '.chain_id'
# expect: 384da888112027f0321850a169f737c33e53b388aad48b5adace4bab97f437e0
# 2) Once synced to head, the head block id must match two independent public endpoints
BLOCK=$(curl -s http://127.0.0.1:8888/v1/chain/get_info | jq -r '.head_block_num')
for API in https://api.protonnz.com https://proton.eosusa.io; do
curl -s "$API/v1/chain/get_block" -d "{\"block_num_or_id\":$BLOCK}" | jq -r '.id'
done
curl -s http://127.0.0.1:8888/v1/chain/get_block -d "{\"block_num_or_id\":$BLOCK}" | jq -r '.id'A mismatched chain id or block id means the snapshot is from another chain or a forked node — discard it.
curl -X POST http://127.0.0.1:8888/v1/producer/create_snapshot#!/bin/bash
# health_check.sh
# Check if nodeos is running
if ! pgrep -x "nodeos" > /dev/null; then
echo "nodeos not running!"
exit 1
fi
# Check head block age
HEAD_TIME=$(curl -s http://localhost:8888/v1/chain/get_info | jq -r '.head_block_time')
HEAD_EPOCH=$(date -d "$HEAD_TIME" +%s)
NOW_EPOCH=$(date +%s)
AGE=$((NOW_EPOCH - HEAD_EPOCH))
if [ $AGE -gt 30 ]; then
echo "Node is $AGE seconds behind!"
exit 1
fi
echo "Node healthy, $AGE seconds behind head"
exit 0nodeos exposes metrics only when the plugin is loaded. Add to config.ini:
plugin = eosio::prometheus_plugin
prometheus-exporter-address = 127.0.0.1:9101Then scrape that address (not 8888):
# prometheus.yml
scrape_configs:
- job_name: 'nodeos'
metrics_path: /v1/prometheus/metrics
static_configs:
- targets: ['localhost:9101']Set up alerts for:
- Node down
- Block production missed (for BPs)
- Sync lag > 30 seconds
- Disk space low
- Memory usage high
./stop.sh
# or
pkill -SIGINT nodeosIf chain state corrupted:
nodeos --replay-blockchainIf blocks corrupted:
nodeos --hard-replay-blockchainDelete all data and resync:
./start.sh --delete-all-blocks --genesis-json genesis.json| Network | Chain ID |
|---|---|
| Mainnet | 384da888112027f0321850a169f737c33e53b388aad48b5adace4bab97f437e0 |
| Testnet | 71ee83bcf52142d61019d95f9cc5427ba6a0d7ff8accd9e2088ae2abeaf3d3dd |
| Port | Service |
|---|---|
| 8888 | HTTP API |
| 9876 | P2P |
| 8080 | State History |
# Node info
cleos get info
# Account info
cleos get account ACCOUNT
# Table query
cleos get table CODE SCOPE TABLE
# Push transaction
cleos push action CONTRACT ACTION 'DATA' -p AUTH
# BP schedule
cleos get schedule
# Producer info
cleos system listproducers| Network | Repository |
|---|---|
| Mainnet | https://github.com/XPRNetwork/xpr.start |
| Testnet | https://github.com/XPRNetwork/xpr-testnet.start |
- Telegram: XPR Network Validators
- Help Desk: https://help.xprnetwork.org