RPC Server for the Pi Network.
This repository contains the Pi RPC server. The RPC server provides a JSON-RPC interface for interacting with the Pi Network. It allows you to:
- Query the current state of the network.
- Submit transactions to the network.
- Fetch transaction status and history.
- Simulate transaction execution (preflight).
The Pi RPC server is designed to be simple, scalable, and compatible with Soroban protocol version 21.2.0.
To build and run pi-rpc, you will need:
- Go: Version 1.25 or higher.
- Rust: Latest stable version (required for Soroban preflight libraries).
- C Compiler: GCC or Clang (for SQLite and FFI components).
- Pi Node: A running
pi-node(orstellar-corecompatible) instance. - Make: For running build automation scripts.
- Ensure all prerequisites are installed.
- Clone the repository:
git clone https://github.com/junman140/pi-rpc.git cd pi-rpc - Build the server and its dependencies:
This will compile the Rust preflight libraries and the Go binary. The resulting binary will be named
make build-pi-rpc
pi-rpc.
Building on macOS is similar to Linux:
- Ensure you have Xcode Command Line Tools installed (
xcode-select --install). - Install Go and Rust using Homebrew or their official installers.
- Build the binary:
make build-pi-rpc
While WSL is the recommended way to build on Windows, you can build natively if you have a C compiler (like MinGW-w64) and make installed:
- Install Go, Rust, and MinGW-w64.
- Open PowerShell and navigate to the repository root.
- Run the build command:
Note: If you encounter issues with
make build-pi-rpc
make, you can manually run the Go and Cargo commands found in the Makefile.
To run the server for development purposes with a local configuration:
- Copy the example configuration file:
cp config.example.toml config.toml
- Edit
config.tomlto match your environment (especiallyPI_NODE_URLandNETWORK_PASSPHRASE). - Start the server:
./pi-rpc --config-path config.toml
For production, it is recommended to use environment variables for configuration and run the server as a systemd service or within a Docker container.
Example using environment variables:
export PI_RPC_NETWORK_PASSPHRASE="Pi Network"
export PI_RPC_PI_NODE_URL="http://localhost:11626"
export PI_RPC_DB_PATH="./pi-rpc.sqlite"
./pi-rpcIn Docker, prefer storing the DB on a mounted data directory (for example /data/pi-rpc.sqlite) so the RPC does not re-ingest from scratch on every restart.
On a new PC, you must build the image locally before docker run will work.
PowerShell (Windows):
docker build -t pi-rpc:local -f cmd/stellar-rpc/docker/Dockerfile .If you see pull access denied / repository does not exist, it means you ran docker run pi-rpc:local without building pi-rpc:local first.
This section assumes:
- you are in the repo root (where
config.pi.tomlandpi-core.cfgexist) - you are using PowerShell on Windows
If you previously ran a container named pi-rpc, remove it:
docker rm -f pi-rpc 2>$nullIf ports are “already allocated”, list what’s running:
docker psdocker build -t pi-rpc:local -f cmd/stellar-rpc/docker/Dockerfile .Confirm the tag exists:
docker images pi-rpcRunning in the background avoids accidental shutdowns (Ctrl+C produces “got signal 2”).
If pi-core.cfg uses a Docker peer name such as testnet:31402, start pi-rpc on the same Docker network as that peer container:
docker run -d --name pi-rpc `
--network pi-net `
-p 8111:8222 -p 8001:8223 `
-v "${PWD}/config.pi.toml:/app/config.pi.toml" `
-v "${PWD}/pi-core.cfg:/app/pi-core.cfg" `
-v pi_rpc_db:/data `
-v pi_captive_core:/captive-core `
pi-rpc:local --config-path /app/config.pi.tomlIn cmd.exe, use %cd% instead of ${PWD} and write the command on one line:
docker run -d --name pi-rpc --network pi-net -p 8111:8222 -p 8001:8223 -v "%cd%\config.pi.toml:/app/config.pi.toml:ro" -v "%cd%\pi-core.cfg:/app/pi-core.cfg:ro" -v pi_rpc_db:/data -v pi_captive_core:/captive-core pi-rpc:local --config-path /app/config.pi.tomlWatch logs:
docker logs -f pi-rpcStop it later:
docker stop pi-rpc- Admin metrics (should load text):
http://localhost:8223/metrics
- RPC health (should return JSON):
$body = @{ jsonrpc = "2.0"; id = 1; method = "getHealth" } | ConvertTo-Json -Compress
Invoke-RestMethod -Method Post -Uri "http://localhost:8222/" -ContentType "application/json" -Body $body- Latest ledger (returns current ledger + headers):
$body = @{ jsonrpc = "2.0"; id = 1; method = "getLatestLedger"; params = @{} } | ConvertTo-Json -Compress
Invoke-RestMethod -Method Post -Uri "http://localhost:8222/" -ContentType "application/json" -Body $bodydocker compose -f monitoring/docker-compose.yml up -d --force-recreate- Grafana:
http://localhost:3001(loginadmin/admin) - Prometheus:
http://localhost:9090(Status → Targets should showpi_rpc_adminas UP)
This is the simplest way to start without managing config files (good for first boot).
PowerShell (Windows):
docker run --rm --name pi-rpc `
-p 8111:8222 -p 8001:8223 `
-e NETWORK="testnet" `
-e ADMIN_ENDPOINT="0.0.0.0:8223" `
pi-rpc:localThis repo includes config.pi.toml and pi-core.cfg. If you don’t mount them (or the mount path is wrong), pi-rpc will refuse to start with:
captive-core-config-path is required, history-archive-urls is required, network-passphrase is required.
PowerShell (Windows):
docker run --rm --name pi-rpc `
-p 8111:8222 -p 8001:8223 `
-v "${PWD}/config.pi.toml:/app/config.pi.toml" `
-v "${PWD}/pi-core.cfg:/app/pi-core.cfg" `
pi-rpc:local --config-path /app/config.pi.tomlImportant: Ensure you are in the root directory of the repository before running the build command.
docker build -t pi-rpc:local -f cmd/stellar-rpc/docker/Dockerfile .The NETWORK environment variable (set to testnet, pubnet, or futurenet) automatically configures captive-core defaults, passphrases, and history archives for Pi environments.
PowerShell (Windows):
docker run -p 8111:8222 -p 8001:8223 `
-e NETWORK="testnet" `
-e ADMIN_ENDPOINT="0.0.0.0:8223" `
pi-rpc:localMount your local config.toml into the container to use your specific Pi settings. Ensure you have set CAPTIVE_CORE_CONFIG_PATH and HISTORY_ARCHIVE_URLS in your config.toml.
PowerShell (Windows):
docker run -p 8111:8222 -p 8001:8223 `
-v "${PWD}/config.toml:/app/config.toml" `
-e ADMIN_ENDPOINT="0.0.0.0:8223" `
pi-rpc:local --config-path /app/config.tomlBash (Linux/macOS/WSL):
docker run -p 8111:8222 -p 8001:8223 \
-v "$(pwd)/config.toml:/app/config.toml" \
-e ADMIN_ENDPOINT="0.0.0.0:8223" \
pi-rpc:local --config-path /app/config.tomlUse the included config.pi.toml and pi-core.cfg:
PowerShell (Windows):
docker run -p 8111:8222 -p 8001:8223 `
-v "${PWD}/config.pi.toml:/app/config.pi.toml" `
-v "${PWD}/pi-core.cfg:/app/pi-core.cfg" `
pi-rpc:local --config-path /app/config.pi.toml- Docker (recommended): edit
config.pi.tomlandpi-core.cfgin this repo, then mount them into the container (see Option B above). - Running the local binary: copy
config.example.tomltoconfig.toml, then edit it.
If pi-rpc starts without these, it exits immediately with:
captive-core-config-path is required, history-archive-urls is required, network-passphrase is required.
In Docker, the easiest working values are:
CAPTIVE_CORE_CONFIG_PATH:/app/pi-core.cfgHISTORY_ARCHIVE_URLS:http://history.testnet.minepi.comNETWORK_PASSPHRASE:Pi Testnet
Those are already set in the repo’s config.pi.toml. The key is: the paths inside the TOML must match where you mount the files inside the container.
- Built the image as
pi-rpcbut ranpi-rpc:local(tags must match). - Ran
docker run pi-rpc:local ...on a new PC without building first (Docker tries to pull from the internet and fails). - Mounted the TOML but used a different
--config-paththan the mount target. - Edited
config.pi.tomlbut forgot to mount it into the container.
If captive-core logs repeat:
Herder: Asking peers for SCP messages more recent than <ledger>
it means core caught up far enough to need live SCP traffic but is not receiving usable consensus messages from peers yet. Common causes:
- Wrong peer chain: a Pi Node launched with
--testnet2is not a valid peer forhistory.testnet.minepi.com/Pi Testnet. - Peer is not caught up: a local
--testnetnode at ledger1/Joining SCPcannot feed live SCP yet. - Outbound networking is restricted (corporate firewall/VPN, strict router rules).
- DNS/connectivity problems inside Docker.
- A misconfigured
pi-core.cfgpeer/quorum setting.
Important notes for Pi Testnet:
- Captive-core TOML parsing runs in strict mode: some stellar-core config keys (for example
KNOWN_PEERS) are rejected. - Use
PREFERRED_PEERS(and optionallyPREFERRED_PEER_KEYS) and setPEER_PORT=31402for Pi Testnet.
This line by itself is not a crash; it’s core waiting to make progress.
pi-rpc uses captive core. For Pi Testnet it needs both:
- history archive access (
http://history.testnet.minepi.com) - live overlay peers on port
31402
If you run a Pi Node container as the peer source, make sure it is on the same chain:
docker inspect testnet2 --format "Image={{.Config.Image}} Cmd={{json .Config.Cmd}}"
docker exec testnet2 sh -lc "wget -qO- http://localhost:11626/info || true"Do not use a container whose command is --testnet2 as the peer for history.testnet.minepi.com. It may be healthy and synced, but it is a different testnet generation and pi-rpc will remain at authenticated_count: 0.
A same-chain peer should report network: Pi Testnet, authenticated peers, and a ledger compatible with the archive head. If local peer containers do not work, use public Pi Testnet peers in pi-core.cfg:
PREFERRED_PEERS=[
"161.35.227.222:31402",
"161.35.227.224:31402",
"161.35.238.87:31402"
]
PREFERRED_PEERS_ONLY=falseAfter changing pi-core.cfg, reset only the captive-core state and restart pi-rpc:
docker stop pi-rpc
docker rm pi-rpc
docker volume rm pi_captive_core
docker run -d --name pi-rpc --network pi-net -p 8111:8222 -p 8001:8223 -v "%cd%\config.pi.toml:/app/config.pi.toml:ro" -v "%cd%\pi-core.cfg:/app/pi-core.cfg:ro" -v pi_rpc_db:/data -v pi_captive_core:/captive-core pi-rpc:local --config-path /app/config.pi.tomlCheck captive-core status:
docker exec pi-rpc sh -lc "wget -qO- http://localhost:11626/info || true"
docker exec pi-rpc sh -lc "wget -qO- http://localhost:11626/peers || true"Healthy progress looks like:
- archive catchup logs finishing with
Catchup finished authenticated_countgreater than0- logs showing
Ingesting ledger <n> - JSON-RPC
getHealthreturning ledger data instead ofDB is empty
If authenticated_count stays 0 after catchup, the TCP port may be open but the peer is not useful for the active chain.
If Docker reports Bind for 0.0.0.0:8222 failed: port is already allocated, another container or process is already publishing RPC ports. Check:
docker ps -a --format "table {{.ID}}\t{{.Names}}\t{{.Status}}\t{{.Ports}}" | findstr /i "8111"If an old standalone pi-rpc container owns 8222-8223, stop/remove it before starting compose or a new standalone container:
docker stop pi-rpc
docker rm pi-rpcDo not run both the standalone pi-rpc container and the pi-dapp-suite compose pi-rpc service on the same host ports.
signal 2 is an interrupt (Ctrl+C). It means the process/container was stopped by the user or by closing the terminal.
Use docker run -d ... + docker logs -f ... (above) to avoid accidentally stopping it.
- RPC endpoint (JSON-RPC):
http://localhost:8222/- You send HTTP POST requests containing JSON-RPC 2.0 payloads.
- Admin endpoint (metrics + pprof):
http://localhost:8223/- Prometheus metrics:
http://localhost:8223/metrics - pprof (debug):
http://localhost:8223/debug/pprof/
- Prometheus metrics:
curl (Linux/macOS/WSL):
curl -sS http://localhost:8222/ \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}'PowerShell (Windows):
$body = @{ jsonrpc = "2.0"; id = 1; method = "getHealth" } | ConvertTo-Json -Compress
Invoke-RestMethod -Method Post -Uri "http://localhost:8222/" -ContentType "application/json" -Body $bodycurl:
curl -sS http://localhost:8222/ \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getLatestLedger","params":{}}'curl:
curl -sS http://localhost:8222/ \
-H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getNetwork","params":{}}'These methods are supported by the server and follow the same JSON-RPC format:
getEventsgetLedgersgetLedgerEntriesgetTransactiongetTransactionssendTransactionsimulateTransactiongetFeeStatsgetVersionInfo
Tip: params are always an object (not an array). (Array params are rejected for backwards-compatibility.)
The server can be configured via command-line flags, environment variables, or a TOML configuration file. Environment variables take precedence over the configuration file, and flags take precedence over everything.
| Flag | Environment Variable | Description |
|---|---|---|
--config-path |
PI_RPC_CONFIG_PATH |
Path to the TOML configuration file. |
--endpoint |
ENDPOINT |
The HTTP endpoint for the RPC server (default: localhost:8222). |
--pi-node-url |
PI_NODE_URL |
URL of the Pi Node instance. |
--network-passphrase |
NETWORK_PASSPHRASE |
Network passphrase for the Pi network. |
--db-path |
DB_PATH |
Path to the SQLite database file. |
--log-level |
LOG_LEVEL |
Minimum log severity (debug, info, warn, error). |
If you encounter 400 Bad Request or Connection refused errors during docker build, it is often due to local network or proxy issues when fetching from Ubuntu mirrors.
Try the following:
- Restart Docker Desktop: Sometimes the internal DNS/network state gets corrupted.
- Clear Docker Cache:
docker builder prune - Use a Different Mirror: You can modify the Dockerfile to use a specific mirror if your local one is down.
This happens if your NETWORK_PASSPHRASE conflicts with the passphrase configured in your CAPTIVE_CORE_CONFIG_PATH or history archive. For Pi Network deployments, prefer running with --config-path /app/config.toml and explicit Pi values instead of the NETWORK shortcut.
Run the Go unit tests:
make go-testRun individual package tests:
go test -v ./cmd/stellar-rpc/internal/configIntegration tests require a running Pi Node environment:
PI_RPC_INTEGRATION_TESTS_ENABLED=true \
PI_RPC_INTEGRATION_TESTS_CORE_MAX_SUPPORTED_PROTOCOL=23 \
PI_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN=$(which pi-node) \
go test -v ./cmd/stellar-rpc/internal/integrationtest/...The admin endpoint already exposes Prometheus metrics at /metrics.
- Start
pi-rpcwith admin endpoint exposed on8001. - Start monitoring stack:
docker compose -f monitoring/docker-compose.yml up -d- Open Grafana at
http://localhost:3001(defaultadmin/admin; port3001avoids clashes with other apps on3000). - The Prometheus datasource is auto-provisioned; use URL
http://prometheus:9090only if you add a datasource manually.
Grafana is pre-provisioned with:
- a Prometheus datasource (
Prometheus) - a starter dashboard (
Pi RPC Overview)
Start/restart monitoring stack:
docker compose -f monitoring/docker-compose.yml up -d --force-recreateOpen Grafana:
http://localhost:3001- Login:
admin/admin - Dashboard: Pi RPC Overview (already loaded)
- Pi RPC Up: whether Prometheus can scrape
pi-rpcadmin metrics (up{job="pi_rpc_admin"}should be1) - Memory / CPU / Goroutines: standard Go process health metrics from the admin
/metricsendpoint
If “Pi RPC Up” is 0, Prometheus cannot reach http://<target>/metrics yet—start by checking http://localhost:8223/metrics in your browser.
If the pi_rpc_admin target is down with lookup host.docker.internal ... no such host, recreate the stack so Prometheus picks up extra_hosts in monitoring/docker-compose.yml:
docker compose -f monitoring/docker-compose.yml up -d --force-recreatepi-rpc must listen on the host at port 8001 (for example docker run ... -p 8001:8223 or a local binary). If you prefer not to use host.docker.internal, edit monitoring/prometheus.yml and set targets to your host IP and port, for example 192.168.x.x:8223.
If docker run reports port is already allocated...8111), stop old containers first:
docker ps
docker stop <container_id>Developer Docs: https://developers.minepi.com (Placeholder) Report Bugs: Please open an issue on the repository.