RelayMiner Setup
What Is a RelayMiner?
A RelayMiner is the software a Supplier runs to actually do the work. It sits between the network and your backends: gateways send it signed requests, it checks that each one is legitimate and paid for, passes it to your backend, signs the answer, and afterwards proves to the chain what it served so you get paid.
The Supplier is your on-chain registration; the RelayMiner is the running process that earns against it. You need both.
The RelayMiner to run is the HA RelayMiner, pocket-relay-miner. It serves every kind of service on the network: blockchain JSON-RPC, WebSocket, gRPC, CometBFT, REST APIs, and streaming responses from LLMs and other long-running services. It runs on a single machine for a first deployment and scales to several machines without changing your stake.
The single-process RelayMiner built into pocketd (pocketd relayminer) is legacy and is no longer recommended. It struggles with REST services, keeps its state on local disk, and cannot fail over. Move existing deployments to the HA RelayMiner; the supplier stake does not change.
How It Works
One deployment is one program run as two processes, sharing one Redis database:
- The relayer answers requests. For each one it checks the signature and the session, confirms the application can still pay, forwards the request to your backend, signs the response, and records the relay. It keeps nothing of its own, so you can run several side by side.
- The miner turns what the relayers recorded into money. After each session it builds a claim, submits it, and later submits the proof the chain asks for, each inside its on-chain window. You can run a second miner as a standby that takes over if the first stops.
- Redis holds every served relay until it has been claimed and proved. It is the ledger, not a cache: anything Redis loses is a claim that cannot be proved.
What It Takes
- A staked Supplier and its operator key. See Supplier Staking.
- A backend for every service you are staked for, such as your own Ethereum node or your own API server.
- A Linux machine with Docker, or systemd if you prefer running binaries. A few CPU cores and around 10 GB of free memory is enough to start.
- Redis 8.10 or newer, configured so it never throws data away.
- A Pocket full node, your own or a public endpoint, for reading the chain and submitting claims and proofs.
- A public HTTPS address on a hostname, pointing at your relayer through a reverse proxy.
- A few POKT in the operator wallet at all times. Every claim and proof is a transaction with a fee; a miner that cannot pay the fee loses that session’s earnings.
The Steps
- Get the image or binary.
- Start Redis with the right memory settings.
- Give the RelayMiner your supplier key.
- Write the relayer configuration: one entry per service, pointing at its backend.
- Write the miner configuration.
- Validate both configurations.
- Start the miner, then the relayer, and check that the relayer reports ready.
- Put a TLS reverse proxy in front and make sure your staked URL points at it.
- Watch the first claims and proofs land.
The Advanced tab walks through each one. The repository’s own deployment runbooks cover the same ground with the expected output of every command.
Running More Than One
As traffic grows, add relayers behind a load balancer and a standby miner, all pointing at the same Redis. Scaling, sizing, monitoring, and upgrades are covered in RelayMiner Operations.
This tab deploys the HA RelayMiner, pocket-relay-miner, for a staked Supplier. The examples use release v0.1.0; check the releases page for the current version and read its notes before deploying.
Prerequisites
| You need | For |
|---|---|
| A staked Supplier and its operator private key | Signing responses, claims, and proofs. See Supplier Staking. |
| A backend for every service the Supplier is staked for, each with a health endpoint the relayer can probe | Answering relays |
Redis 8.10 or newer, with maxmemory set and maxmemory-policy noeviction | Shared state. Both processes refuse to start otherwise. |
| A Pocket full node: CometBFT RPC and gRPC | Chain queries, and submitting claims and proofs |
| A funded operator account | Claim and proof transaction fees |
| Docker with Compose v2, or a Linux host with systemd and cgroup v2 | Running the processes |
The miner’s block_time_seconds and chain_id must match the network:
| Network | chain_id | block_time_seconds | Public endpoints |
|---|---|---|---|
| Beta TestNet | pocket-lego-testnet | 30 | https://sauron-rpc.beta.infra.pocket.network, sauron-grpc.beta.infra.pocket.network:443 |
| MainNet | pocket | 60 | https://sauron-rpc.infra.pocket.network, sauron-grpc.infra.pocket.network:443 |
Start on Beta TestNet.
Invariants
Each of these stops a deployment if it is broken:
- The relayer and the miner run the same version. Mixed versions are not supported.
- Redis 8.10+,
maxmemoryset,noeviction. Checked at process start, not byvalidate. GOMEMLIMITandGOMAXPROCSset, or container or systemd memory and CPU limits. Without them each process sizes its pools against the whole host.- The miner has
block_time_secondsand the rightpocket_node.chain_id, and the node is reachable. Otherwise it exits. - The miner runs before relays are expected. The relayer’s
/readyanswers 503 until the miner has published its service manifest to Redis. validateexits 0 on the exact configuration files you start.
1. Get the Image or Binary
Docker:
docker pull ghcr.io/pokt-network/pocket-relay-miner:v0.1.0Or the binary, for a host deployment (arm64 instead of amd64 on ARM):
curl -sLO https://github.com/pokt-network/pocket-relay-miner/releases/download/v0.1.0/pocket-relay-miner_v0.1.0_linux_amd64.tar.gz
curl -sLO https://github.com/pokt-network/pocket-relay-miner/releases/download/v0.1.0/checksums.txt
sha256sum --ignore-missing -c checksums.txt
tar -xzf pocket-relay-miner_v0.1.0_linux_amd64.tar.gz
sudo install pocket-relay-miner /usr/local/bin/Clone the repository at the same tag for its example configurations, systemd units, and config.redis.example.conf:
git clone --branch v0.1.0 https://github.com/pokt-network/pocket-relay-miner.git2. Start Redis
Load config.redis.example.conf from the repository and set maxmemory for your host, leaving headroom below the memory Redis may use. Run Redis on its own; it holds every claim tree and must not be shared with other applications. Never expose it beyond 127.0.0.1 or a private network.
docker run -d --name relay-miner-redis --restart unless-stopped \
-p 127.0.0.1:6379:6379 \
-v "$PWD/pocket-relay-miner/config.redis.example.conf:/usr/local/etc/redis/redis.conf:ro" \
-v relay-miner-redis-data:/data \
redis:8.10.1-alpine redis-server /usr/local/etc/redis/redis.conf --maxmemory 4gbVerify:
docker exec relay-miner-redis redis-cli --raw INFO server | grep redis_version # 8.10 or newer
docker exec relay-miner-redis redis-cli --raw CONFIG GET maxmemory-policy # noevictionDistribution packages often ship an older Redis. Use the Redis project’s packages or the official image.
3. Supplier Keys
Both processes sign with the Supplier’s operator key. Configure exactly one key source; setting both is a startup error.
A keys file, the simplest start:
# supplier-keys.yaml, mode 0600, readable only by the RelayMiner's user
keys:
- "<operator private key, 64 hex characters>"Export the key from the keyring it was created in:
pocketd keys export <operator-key-name> --unarmored-hex --unsafe --keyring-backend fileA pocketd keyring, recommended for production. Only the file and test backends are accepted:
keys:
keyring:
backend: "file"
dir: "/keys/keyring" # the directory that CONTAINS keyring-file
app_name: "pocket"
passphrase_file: "/secrets/keyring-passphrase"
key_names: ["operator"] # optional; empty loads every keyKeys reload while the processes run (keys.hot_reload_enabled, on by default), so adding a Supplier does not need a restart. One relayer and miner pair can sign for many Suppliers: list every operator key.
4. Relayer Configuration
The relayer reads its configuration once, at startup. Every key, its default, and why to change it is in config.relayer.example.yaml.
# relayer.yaml
listen_addr: "0.0.0.0:8080" # relay traffic; the reverse proxy forwards here
redis:
url: "redis://127.0.0.1:6379"
pocket_node:
query_node_rpc_url: "https://sauron-rpc.beta.infra.pocket.network"
query_node_grpc_url: "sauron-grpc.beta.infra.pocket.network:443"
grpc_insecure: false # true only for a plain-text gRPC node
keys:
keys_file: "/etc/pocket-relay-miner/supplier-keys.yaml"
services:
# One entry per service ID the Supplier is staked for.
eth:
validation_mode: eager
default_backend: jsonrpc
backends:
jsonrpc:
urls:
- "http://10.0.0.5:8545"
- name: backup
url: "http://10.0.0.6:8545"
health_check:
enabled: true
endpoint: "/"
method: "POST"
request_body: '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
expected_body: '"result"'
interval_seconds: 10
timeout_seconds: 5
unhealthy_threshold: 3
healthy_threshold: 2
websocket:
url: "ws://10.0.0.5:8546" # no health_check on websocket or grpc
my-rest-api:
validation_mode: eager
timeout_profile: fast # streaming for LLMs and SSE
default_backend: rest
backends:
rest:
url: "http://10.0.0.7:8080"
health_check:
enabled: true
endpoint: "/v1/health"
expected_body: '"ok"'
health_check:
enabled: true
addr: "127.0.0.1:8081"
metrics:
enabled: true
addr: "127.0.0.1:9090"
logging:
level: "info"
format: "json"Services and Backends
| Key | Meaning |
|---|---|
services.<id> | An on-chain service ID the Supplier is staked for. A relay for a service not listed here is rejected. Never invent an ID; find the right one from the service’s card. |
default_backend | The backend a relay goes to when it does not name a transport. Defaults to jsonrpc, so a REST-only service must set rest. |
backends.<transport> | One backend pool per transport. The key must be jsonrpc, rest, websocket, grpc, or cometbft. Relays are routed by exact transport, with no fallback. |
url / urls | One backend, or a pool. A urls entry is a URL or a {name, url} pair. |
load_balancing | round_robin or first_healthy. Defaults to round robin for two or more URLs. |
base_path | A path prefix the backend expects on every request, such as /ext/bc/C/rpc. |
headers, authentication | Static headers, and one of bearer_token, plain_token, or username and password, sent on every request to the backend. |
health_check | Active probing. Turn it on for every HTTP backend (jsonrpc, rest, cometbft); leave it off for websocket and grpc, where the HTTP probe always fails. |
validation_mode | eager validates a relay before serving it; optimistic serves first and validates afterwards. The global default is default_validation_mode: optimistic. |
timeout_profile | fast (30 s) or streaming (600 s, no response-header timeout), or a profile you define under timeout_profiles. |
pool_profile | A per-service connection pool: low, medium, high, or your own under pool_profiles. |
max_body_size_bytes | The body limit for the service. Request bodies are held in Redis until the claim, so this bounds Redis use. |
The card’s transport names map to backend keys like this:
rpc_type (stake file, service card) | Backend key |
|---|---|
JSON_RPC | jsonrpc |
WEBSOCKET | websocket |
REST | rest |
GRPC | grpc |
COMET_BFT | cometbft |
Serve every transport the card expects: one backend per transport here, and one endpoint per transport in the stake file.
Why Health Checks Matter
Without an active health check the relayer learns a backend is down only from the relays it forwards: it marks the backend unhealthy after 5 consecutive failures and retries it every 30 seconds with real relays. Each of those failures is a relay a gateway sent you that got an error. With a health check, the relayer probes on its own and routes around a failing backend before a relay reaches it. Probe something that proves the node can answer, not only that its port is open.
5. Miner Configuration
Every key is in config.miner.example.yaml.
# miner.yaml
redis:
url: "redis://127.0.0.1:6379" # the same Redis as the relayer
pocket_node:
query_node_rpc_url: "https://sauron-rpc.beta.infra.pocket.network"
query_node_grpc_url: "sauron-grpc.beta.infra.pocket.network:443"
chain_id: "pocket-lego-testnet" # pocket on MainNet
grpc_insecure: false
keys:
keys_file: "/etc/pocket-relay-miner/supplier-keys.yaml"
block_time_seconds: 30 # 60 on MainNet
metrics:
enabled: true
addr: "127.0.0.1:9092"
logging:
level: "info"
format: "json"The miner submits the claims and proofs you are paid for. Public endpoints are fine for a first deployment; a production miner should use a full node you control. The miner’s balance monitor logs a warning when the operator balance falls below balance_monitor.balance_threshold_upokt (1 POKT by default).
6. Validate
Validate both files with the same binary you will run. --check-stake also lists staked services that have no backend:
pocket-relay-miner miner validate --config miner.yaml
pocket-relay-miner relayer validate --config relayer.yaml --check-stakeEach should print config OK: ... would start and exit 0. validate reports every unknown or retired key in one pass, with its line number.
7. Start
Start Redis, then the miner, then the relayer.
Docker Compose. The repository’s compose example runs Redis, the miner, and the relayer with memory limits, health checks, and the right startup order, pointed at Beta TestNet. It ships with a public, unstaked key so the stack starts as cloned; replace it with your own keys file before serving.
docker compose -p pocket-supplier -f examples/docker-compose/docker-compose.yaml up -dsystemd. The repository ships unit files and an environment file in examples/host. Set GOMEMLIMIT and GOMAXPROCS in the environment file, then:
sudo systemctl enable --now pocket-relay-miner-miner
sudo systemctl enable --now pocket-relay-miner-relayerCheck that the relayer is ready and each backend is healthy:
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:8081/ready # 200 once it can serve
curl -s http://127.0.0.1:8081/ready/eth # each backend's state for one service8. Put a Reverse Proxy in Front
The relayer speaks plain HTTP. Terminate TLS on a reverse proxy such as Caddy, nginx, or a cloud load balancer, and forward to the relayer’s listen_addr. The proxy’s https:// hostname is the publicly_exposed_url in your stake; gateways can refuse plain-HTTP and raw-IP URLs. Do not add authentication at the proxy; every relay is already signed.
With more than one relayer, the load balancer spreads traffic across them. They share all state through Redis, so no session stickiness is needed.
9. Watch Claims and Proofs
After a session with served relays ends, the miner submits its claim and then, in the proof window, its proof. Read the submission record straight from Redis:
pocket-relay-miner redis --config miner.yaml submissions --supplier <operator address>Each claimed session shows a row whose claim and proof status turn to success once the proof window has passed. On-chain, confirm with:
pocketd query proof list-claims --supplier-operator-address <operator address> --network=betaTo test a live relayer without a staked application, the repository’s simulated relays send a real signed request to a real backend without charging or claiming anything.
Switching to MainNet
In both files, switch pocket_node to the MainNet endpoints; in the miner, also set chain_id: pocket and block_time_seconds: 60. Validate both again, then restart the miner and then the relayer.
Troubleshooting
| Symptom | Cause | Action |
|---|---|---|
Error: config is INVALID | A key is unknown, retired, or has a bad value | Fix the key validate names, and run it again until it exits 0. |
redis maxmemory is 0 or maxmemory-policy is "..." | Redis has no memory limit, or evicts | Load config.redis.example.conf and set maxmemory. |
cannot start without the node's network | The miner cannot reach the node’s gRPC | Check query_node_grpc_url and grpc_insecure. |
the node reports network "..." | chain_id does not match the node | Set the network’s chain_id. |
/ready answers 503 no service factor manifest | The miner is not running yet | Start the miner, then wait. |
no memory limit found | No GOMEMLIMIT and no container limit | Set limits. |
Relays answered 429 storage saturated | Redis is nearly full | Raise maxmemory, or let the miner drain. |
Relays refused with no_local_signer | The relayer does not hold the key for the Supplier the relay names | Add the operator key. |
Per-relay rejections are logged at debug level only; count them with ha_relayer_relays_rejected_total on the relayer’s metrics port. The repository’s troubleshooting guide lists every error message with its fix.