XChain Platform Explorer: Configuration

Configuration Sources

The explorer resolves configuration from multiple sources in priority order:

  1. Environment variables: loaded from .env via dotenv
  2. xchain-hub: fetched via JSON-RPC on startup and refreshed every 60 seconds
  3. Local config.json: fallback file at src/config.json
  4. NODE_CONFIG: JSON string environment variable (alternative to config.json file)

Hub-sourced configuration takes precedence for database connection details, allowing centralized management across all explorer instances. Environment variables control server-level settings (ports, SSL, debug mode).

Environment Variables

Server Settings

Variable Required Default Description
EXPLORER_API_PORT_HTTP No 8080 HTTP server port
EXPLORER_API_PORT_HTTPS No 8081 HTTPS server port
API_HOST No 127.0.0.1 Bind address for the API server
DEBUG No None Enable debug output when set to any truthy value
EXPLORER_FORCE_HTTPS No None Explicitly enable (1) or disable (0) the HTTPS-hardening headers (HSTS and upgrade-insecure-requests). By default they are active only when NODE_ENV=production, so plain-HTTP dev/regtest deploys are not broken. Set to 1 when running behind a TLS-terminating proxy without NODE_ENV=production.
EXPLORER_HOLDERS_CACHE_MS No 15000 TTL (ms) of the per-tick holders-query result cache. getHolders requires an unindexable full-table filesort, so results are cached briefly to bound repeated-query cost.
EXPLORER_HOLDERS_CACHE_MAX No 500 Maximum number of distinct holders-query results kept in the cache.
EXPLORER_TOTALS_CACHE_MS No 60000 TTL (ms) of the platform-totals query cache.
MEMPOOL_COUNT_CACHE_MS No 15000 TTL (ms) of the mempool-count cache.
PRICE_CACHE_MS No 60000 TTL (ms) of the oracle-price cache.
FEE_CACHE_MS No 60000 TTL (ms) of the fee-schedule cache.
EXPLORER_WALLET_URL No https://wallet.xchain.io Wallet handoff target for the contract page’s Write Contract card. Set it to an empty string to disable the card entirely; the default applies only when the variable is unset, never when it is set to ''.
ENCODER_URL No None Encoder base URL used for the UI’s fee estimate. Unset returns a conservative {low:1, medium:2, high:3} fallback rather than an error.
UTXO_TRACKER_URL_<COIN> No None Per-coin UTXO-tracker base URL, used to fill the address page’s balance and UTXO panel from the tracker’s GET /info/<address>. COIN is the route code (BTC, TBTC, RDOGE, …).
UTXO_TRACKER_URL No None Fallback tracker base URL when no coin-specific variable is set. With neither configured the address page shows an honest “Unavailable” rather than fabricated zeroes.
HUB_URL No None Hub base URL used to fetch the coin’s USD price for the UI. Unset leaves the $0.00 placeholder, which is also what testnet and regtest route codes get, since the oracle only prices mainnet assets.
EXPLORER_TIP_MAX_AGE_S No 21600 Age (seconds) past which a coin’s newest indexed block counts as stale. A stale coin is reported stale: true by /{COIN}/api/status and dropped from that response’s available map, so consumers stop treating this instance as current for it. The gate fails closed: a missing or unreadable block_time also reads stale. Set to 0 to disable the gate for every coin.
EXPLORER_TIP_MAX_AGE_S_<COIN> No value of EXPLORER_TIP_MAX_AGE_S Per-coin override of the tip-age threshold, where COIN is the route code (BTC, TBTC, RDOGE, …). Use it where one chain’s block interval makes the shared default wrong.

WebSocket

Variable Required Default Description
WS_ENABLED No true Enable/disable WebSocket server. The WebSocket server is disabled ONLY when this variable is set to the exact string false. Any other value (including 0, empty string, or unset) leaves it enabled.
WS_POLL_INTERVAL No 5000 Change detection poll interval in milliseconds
WS_PING_INTERVAL No 30000 Server-to-client ping interval in milliseconds
WS_IDLE_TIMEOUT No 300000 Idle timeout for zero-subscription clients (ms)
WS_MAX_CONNECTIONS_PER_IP No 5 Max concurrent WebSocket connections per IP
WS_MAX_SUBSCRIPTIONS No 25 Max subscriptions per WebSocket connection
WS_MAX_BACKPRESSURE No 65536 Max buffered bytes before skipping messages for a slow client

See WEBSOCKET.md for the full WebSocket API reference.

Hub Connection

Variable Required Default Description
HUB_API_HOST No localhost xchain-hub hostname for config discovery (single-instance; ignored when HUB_VALIDATORS is set)
HUB_PORT No 10000 xchain-hub port (single-instance; ignored when HUB_VALIDATORS is set)
HUB_VALIDATORS No None Comma-separated list of hub URLs for high-availability config discovery (e.g. http://hub1:10000,http://hub2:10000). When set, takes precedence over HUB_API_HOST/HUB_PORT and the explorer tries each URL in order, falling back to the next on failure.
EXPLORER_MAX_CONCURRENT_REQUESTS No 200 Concurrency cap for API requests. Past the cap a request is refused immediately with 429 and Retry-After: 1 rather than queued, so a burst degrades into fast rejections instead of a growing backlog. Static assets are exempt. Set to 0 to disable the cap
UPDATE_CONFIG_INTERVAL No 60000 Interval in milliseconds between hub config refresh polls
HUB_RETRY_ATTEMPTS No 4 Attempts per hub config fetch, with exponential backoff. After a power cycle the hub and its MariaDB can take several seconds to come up; a single-pass fetch loses that race and leaves the explorer with no config. ping() opts out so liveness checks stay fast.
HUB_RETRY_DELAY_MS No 2000 Base backoff between hub config retry attempts. Tests set 0.
HUB_DB_SYNC_POLL_INTERVAL No 30000 Interval in milliseconds between hub-mirror table sync polls.
HUB_SYNC_WATERMARK_INTERVAL_MS No 10000 Interval in milliseconds at which the hub-mirror sync persists its progress watermark.
MIRROR_DB_PASS No None Password for the hub-mirror schema migration tool, read only by bin/migrate-hub-mirror.js and never by the running explorer. Passed in the environment specifically so it stays off the command line: MIRROR_DB_PASS=… node bin/migrate-hub-mirror.js --host … --user … --schema …. Treat as a credential.
CONFIG_CACHE_FILE No <appdir>/tmp/config-cache.json Path to the on-disk last-known-good hub config cache. The explorer writes here after each successful hub fetch and reads it on startup when the hub is unreachable, so it comes up serving the last known coin set rather than zero coins. Override to a mounted volume path to survive container recreation.
NO_HUB No None Set to 1 (or true/yes) to enable standalone mode: the hub is not contacted and all coin/network + database config is read from src/config.json (or NODE_CONFIG). Use on single-server deployments where the hub publishes docker-internal DB hosts that are not reachable from the explorer process.

Hub Mirror and Operational Reads

Variable Required Default Description
HUB_API_URL No None Hub base URL(s), comma-separated. Used by two features: the self-synced checkpoint mirror (database.checkpoint.self_sync) and the JSON-RPC reads that back the validator-capabilities and governance pages. Works in NO_HUB mode too, so a standalone node can still point these reads at a hub.
HUB_API_KEY No None API key for the hub’s /hub-db mirror feed, when the hub operator has configured one.
EXPLORER_HUB_CACHE_MS No 15000 How long (ms) validator-capabilities and governance rows fetched from the hub are cached before re-fetching.
EXPLORER_HUB_CACHE_STALE_MAX_MS No 600000 How long (ms) previously-fetched rows may still be served while the hub is unreachable. Past this, the pages fail rather than serve very old data.
MIRROR_MAX_LAG_S No None For self-synced mirrors: log a warning when the mirror lags the hub by more than this many seconds. Responses always carry mirror_lag_seconds so clients can judge freshness themselves.
MIRROR_LAG_FAIL_CLOSED No None Set to 1 to return HTTP 503 (MIRROR_STALE) instead of only warning when MIRROR_MAX_LAG_S is exceeded.
SPV_CHECKPOINT_MAX_LAG_BLOCKS No 100 Advisory freshness threshold for the SPV proof endpoints: when the serving checkpoint trails the chain tip by more than this many blocks, responses set stale: true (alongside chain_tip and lag). Advisory only; nothing is refused.
ALLOW_NO_COLOCATED_HUB_DB No None Set to 1 to let the explorer start without a checkpoint schema configured for every serving coin. The checkpoint, cross-chain match, and proof endpoints then fail per request instead.

Decoder Health (for /api/status chain lag fields)

The explorer polls each coin’s decoder health endpoint to populate chain_tip, chain_lag_blocks, and decoder_health in /api/status. Configure the URL of each decoder’s JSON-RPC API:

Variable Required Default Description
DECODER_API_URL_<COIN>_<NETWORK> No None Decoder JSON-RPC URL for a specific coin+network (e.g. DECODER_API_URL_BTC_MAINNET=http://localhost:4001). COIN and NETWORK are uppercase.
DECODER_API_URL No None Generic fallback used when no coin/network-specific variable is set

When no decoder URL is configured for a coin, decoder_health is "unconfigured" and chain_tip/chain_lag_blocks are null for that coin.

Indexer API (native-coin fee pre-flight)

The public /{COIN}/api/feequote and /{COIN}/api/feeschedule endpoints proxy to the colocated xchain-indexer JSON-RPC API (which is not internet-facing) so the authoritative fee + oracle-price logic stays single-sourced. Configure the per-coin indexer API URL to enable them; when unset, those two endpoints return 503 (clients then fall back to paying the protocol fee in XCHAIN).

Variable Required Default Description
INDEXER_API_URL_<COIN>_<NETWORK> No None Indexer JSON-RPC URL for a specific coin+network (e.g. INDEXER_API_URL_BTC_REGTEST=http://localhost:3004)
INDEXER_API_URL No None Generic fallback indexer JSON-RPC URL used when no coin/network-specific var is set
INDEXER_API_TIMEOUT_MS No 5000 Per-request timeout for the indexer proxy calls
EXPLORER_FEEQUOTE_BUSY_RETRY_MS No 6000 Wall-clock budget for re-asking /{COIN}/api/feequote while the indexer answers busy: true, retryable: true (it is processing a block). This hop absorbs the overlap because the wallet reads the endpoint on every fee-bearing compose and has no retry of its own. Only a retryable busy answer is re-asked; a verdict never is.
EXPLORER_INDEXER_API_KEY No None API key presented to the indexer’s fail-closed federation-read gate. When the peer indexer sets INDEXER_API_KEY, gated methods such as getstakeweightsbycapability return 401 without this, which is what a hardened indexer needs in order to still serve the explorer’s validator-set proof.
DECODER_API_TIMEOUT_MS No 2500 Per-request timeout for decoder health calls. Tighter than the indexer timeout on purpose: health aggregation runs on the /api/status hot path, so a stalled decoder must not hold the whole status response.

Contract simulation (Read Contract card)

The explorer can run a read-only contract call in the VM to power the contract page’s Read Contract card. It is off by default: the endpoint exists only when explicitly enabled.

Variable Required Default Description
EXPLORER_VM_QUERY_ENABLED No false Set to true to enable POST /{COIN}/api/contract/{idx}/call and advertise the Read Contract card. Any other value leaves the route disabled.
EXPLORER_VM_QUERY_RATE_LIMIT_RPM No 20 Simulation requests per minute per IP
EXPLORER_VM_MAX_CONCURRENT No 4 Global ceiling on concurrent simulations
EXPLORER_VM_MAX_CONCURRENT_PER_IP No half the global pool, minimum 1 Per-IP share of the simulation slot pool, so a small set of clients cannot monopolize every slot
EXPLORER_VM_MAX_STATE_BYTES No 4194304 (4 MiB) Byte cap on the initial contract-state load. The VM’s own limits bound only new state writes, so without this cap a caller can aim simulations at a contract with huge accumulated state and burn SQL, JSON.parse, and IPC on every call.

Enabling it requires a canonical vendored VM

Setting EXPLORER_VM_QUERY_ENABLED=true is not sufficient on its own. Before it serves a simulation the explorer checks the vendored xchain-vm it loaded, and refuses when that VM is not the consensus epoch this explorer expects or is missing the contract-era gate exports. A refusing endpoint answers 503 with code VM_QUERY_VM_DRIFT and names the reason, and the same reason is logged once at boot whenever the flag is on.

The check exists because a deployed explorer’s bundled VM can go stale silently: the vendored copy is staged by the rollout, not by the git checkout, and its version string moves by a patch while its bytes move by a hundred kilobytes. Simulating in a stale VM would answer contract calls with results the indexers do not agree with, on the same service that serves contract-state proofs, so the endpoint fails closed instead.

The in-process check is coarser than a byte comparison, because a running process has no canonical copy to compare against. The full comparison is bin/check-explorer-vm-drift.sh <host> in the platform checkout: read-only over SSH, it hashes the deployed VM tree against canonical and reads the flag out of the running process. Run it before enabling the flag on a public explorer, and enable only once it reports OK.

SSL/TLS

Variable Required Default Description
SSL_DIR No src/ssl/ Directory containing SSL certificate files

SSL certificates are loaded from:

  • {SSL_DIR}/cert.pem: TLS certificate
  • {SSL_DIR}/private.pem: TLS private key
  • {SSL_DIR}/ca.pem: Certificate authority chain

If SSL files are not found, only the HTTP server starts.

Font Awesome kit

The web UI loads its generic glyphs from a Font Awesome kit. The kit token is an account credential and the pro license flag is an entitlement claim, so both are supplied per deployment instead of being shipped in the source tree. The explorer assembles /js/fontawesome-kit.js at request time from the vendored kit loader plus these values.

Variable Required Default Description
EXPLORER_FONTAWESOME_KIT_TOKEN No None Kit token. Unset means no kit is loaded.
EXPLORER_FONTAWESOME_KIT_ID No None Numeric kit id.
EXPLORER_FONTAWESOME_KIT_LICENSE No free Entitlement this deployment holds: free or pro.
EXPLORER_FONTAWESOME_KIT_VERSION No 6.4.0 Font Awesome version the kit serves.
EXPLORER_FONTAWESOME_KIT_CUSTOM_ICONS_PATH No None Path of the kit’s uploaded-icons stylesheet, if it has one.

With no token set, /js/fontawesome-kit.js returns an inert stub and the explorer makes no requests to fontawesome.com. The UI still renders: the coin/network glyphs in the navigation bar are local images served from the explorer itself, so only the generic Font Awesome glyphs are absent.

A value that does not match its expected shape (a non-numeric kit id, a license other than free/pro) is ignored with a warning rather than passed through to the browser.

Local Configuration File

The src/config.json file provides database connection details when xchain-hub is not available. Structure:

{
    "databases": {
        "BTC": {
            "mainnet": {
                "host": "localhost",
                "port": 3306,
                "user": "xchain_reader",
                "password": "your_password",
                "indexer": "XChain_BTC_Mainnet_Indexer",
                "decoder": "XChain_BTC_Mainnet_Decoder"
            },
            "testnet": { ... },
            "regtest": { ... }
        },
        "LTC": { ... },
        "DOGE": { ... }
    }
}

Each coin/network entry specifies both the Indexer database (primary data source) and the Decoder database (for raw transaction lookups).

An example template is provided at src/config.json.example.

Checkpoint Schema (Hub-Mirror Tables)

A few tables the explorer serves (state checkpoints, capability snapshots, cross-chain matches) are produced by the hub federation rather than the indexer, and xchain-sync never replicates them. Every serving coin/network therefore needs a checkpoint block in its database config, naming a schema on the same server and credentials as the indexer database:

"checkpoint": {
    "host": "localhost",
    "port": 3306,
    "user": "xchain_reader",
    "pass": "your_password",
    "name": "XChain_Hub_Mirror",
    "self_sync": true
}

There are two ways to provision that schema:

  • Self-synced (recommended): set "self_sync": true and configure HUB_API_URL. The explorer creates the schema and its tables itself, downloads a snapshot from the hub, and then follows the hub’s live feed. No hub database needs to exist on the explorer’s server.
  • Externally maintained: omit self_sync and point name at a real hub database on the same server (single-server deployments where the hub already runs locally).

Without a checkpoint block for a serving coin, the explorer refuses to start (see ALLOW_NO_COLOCATED_HUB_DB).

In self-sync mode the affected endpoints return HTTP 503 with code MIRROR_NOT_BOOTSTRAPPED until the first snapshot download completes, and afterwards include mirror_bootstrapped and mirror_lag_seconds fields so clients can judge freshness. GET /{COIN}/api/hub-mirror/status reports the mirror’s state. One detail to know: the anchor_txid audit field on cross-chain matches is filled in by the hub after anchor publication; the hub re-broadcasts the stamped row on the mirror feed, so a self-synced mirror picks it up shortly after the anchor lands (a mirror that was offline at that moment catches up on its next bootstrap). The legacy batch_root field only exists on rows stamped by a retired publisher and arrives with the snapshot. All trade-relevant fields arrive immediately.

Hub Discovery

When HUB_API_HOST and HUB_PORT are set, the explorer:

  1. Connects to xchain-hub via JSON-RPC (ping to verify, then getallconfigs to fetch)
  2. Receives database connection details for all configured coins and networks
  3. Determines which coins are supported (defined in config) vs available (database reachable)
  4. Starts a 60-second sync interval to refresh configuration

If the hub is unreachable, the explorer falls back to src/config.json or the NODE_CONFIG environment variable.

Config Change Events

The config module provides an event system for live updates:

const config = require('./config.js');
config.onConfigChanged(() => {
    // React to configuration changes (e.g., new coins available)
});

Coin-Specific Configuration

Each supported blockchain has a configuration file in src/configs/:

File Chain
src/configs/BTC.js Bitcoin
src/configs/LTC.js Litecoin
src/configs/DOGE.js Dogecoin

These files export a getConfig(network) function returning:

{
    chain: {
        name: "Bitcoin",
        tick: "BTC",
        site: "https://bitcoin.org"
    },
    address: {
        burn:      "...",   // Token burn address
        gas:       "...",   // XCHAIN gas token issuer
        protocol:  "...",   // Protocol development fund
        community: "...",   // Community development fund
        explorer:  "..."    // Explorer service address
    }
}

Addresses vary per network (mainnet, testnet, regtest).

Supported Coins and Prefixes

The config module builds a mapping of supported coins from the configuration:

Coin Mainnet Prefix Testnet Prefix Regtest Prefix
Bitcoin BTC TBTC RBTC
Litecoin LTC TLTC RLTC
Dogecoin DOGE TDOGE RDOGE

These prefixes form the first segment of all API URLs (e.g., /BTC/api/token/MYTOKEN).

Rate Limiting

The explorer uses express-rate-limit middleware:

Setting Value
Window 60 seconds
Max requests per window 500 (override via EXPLORER_RATE_LIMIT_RPM)
Scope Per IP address
Response on limit HTTP 429 Too Many Requests
Variable Required Default Description
EXPLORER_RATE_LIMIT_RPM No 500 Maximum requests per IP per 60-second window. Image requests (.png, .jpg, .jpeg, .gif, .ico, .svg, .webp), /icon/ paths, and /images paths are excluded from the limit.
EXPLORER_ACTION_PROOF_RATE_LIMIT_RPM No 60 Separate, tighter limit for /{COIN}/api/proof/action/{idx}
EXPLORER_VALIDATOR_SET_PROOF_RATE_LIMIT_RPM No 30 Separate, tighter limit for /BTC/api/proof/validator-set
WS_TRUST_PROXY_HOPS No 1 Proxy hop count used to resolve the real client address for the WebSocket per-IP cap. The upgrade is handled on the raw HTTP server, where Express’s trust proxy does not apply, so the hop count must be passed explicitly or the cap keys on a spoofable X-Forwarded-For. Keep it aligned with the HTTP side.

Rate limiting applies to all non-image endpoints (API, Explorer, and HTML).

CORS

Cross-Origin Resource Sharing is configured via the cors middleware. Allowed origins can be specified in the configuration. When not explicitly configured, CORS defaults to allowing all origins.

Security Headers

The explorer uses Helmet middleware to set security headers including:

  • Content Security Policy (CSP)
  • X-Content-Type-Options: nosniff
  • X-Frame-Options
  • Strict-Transport-Security (when HTTPS is active)
  • X-XSS-Protection

Finality / Confirmation Depths

The explorer uses per-chain confirmation depths to determine when a receipt is considered final. These values are surfaced in the finality field of the /api/network response and mirror the hub’s cross-chain thresholds. They can be overridden per chain:

Variable Required Default Description
XCHAIN_CONFIRMATIONS_BTC No 6 Required confirmation depth for Bitcoin
XCHAIN_CONFIRMATIONS_LTC No 12 Required confirmation depth for Litecoin
XCHAIN_CONFIRMATIONS_DOGE No 60 Required confirmation depth for Dogecoin

Database

The explorer reads from MariaDB databases following the naming convention:

XChain_{CHAIN}_{NETWORK}_Indexer    (primary: indexed state)
XChain_{CHAIN}_{NETWORK}_Decoder    (secondary: raw transaction data)

Connection pooling is managed by the mariadb npm package. The explorer maintains separate pool connections for Indexer and Decoder databases.

All queries use parameterized SQL (? placeholders) to prevent SQL injection. No ORM is used.


Copyright © 2025–2026 Dankest, LLC

Based on XChain Platform by Dankest, LLC – https://dankest.llc

Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.

You may use, modify, and distribute this material under the terms of the License. See LICENSE and NOTICE for full terms. See the licensing overview.

Edit this page on GitHub ↗