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.

Deprecated

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

  1. Get the image or binary.
  2. Start Redis with the right memory settings.
  3. Give the RelayMiner your supplier key.
  4. Write the relayer configuration: one entry per service, pointing at its backend.
  5. Write the miner configuration.
  6. Validate both configurations.
  7. Start the miner, then the relayer, and check that the relayer reports ready.
  8. Put a TLS reverse proxy in front and make sure your staked URL points at it.
  9. 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 needFor
A staked Supplier and its operator private keySigning 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 probeAnswering relays
Redis 8.10 or newer, with maxmemory set and maxmemory-policy noevictionShared state. Both processes refuse to start otherwise.
A Pocket full node: CometBFT RPC and gRPCChain queries, and submitting claims and proofs
A funded operator accountClaim and proof transaction fees
Docker with Compose v2, or a Linux host with systemd and cgroup v2Running the processes

The miner’s block_time_seconds and chain_id must match the network:

Networkchain_idblock_time_secondsPublic endpoints
Beta TestNetpocket-lego-testnet30https://sauron-rpc.beta.infra.pocket.network, sauron-grpc.beta.infra.pocket.network:443
MainNetpocket60https://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:

  1. The relayer and the miner run the same version. Mixed versions are not supported.
  2. Redis 8.10+, maxmemory set, noeviction. Checked at process start, not by validate.
  3. GOMEMLIMIT and GOMAXPROCS set, or container or systemd memory and CPU limits. Without them each process sizes its pools against the whole host.
  4. The miner has block_time_seconds and the right pocket_node.chain_id, and the node is reachable. Otherwise it exits.
  5. The miner runs before relays are expected. The relayer’s /ready answers 503 until the miner has published its service manifest to Redis.
  6. validate exits 0 on the exact configuration files you start.

1. Get the Image or Binary

Docker:

bash
docker pull ghcr.io/pokt-network/pocket-relay-miner:v0.1.0

Or the binary, for a host deployment (arm64 instead of amd64 on ARM):

bash
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:

bash
git clone --branch v0.1.0 https://github.com/pokt-network/pocket-relay-miner.git

2. 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.

bash
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 4gb

Verify:

bash
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         # noeviction

Distribution 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:

yaml
# 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:

bash
pocketd keys export <operator-key-name> --unarmored-hex --unsafe --keyring-backend file

A pocketd keyring, recommended for production. Only the file and test backends are accepted:

yaml
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 key

Keys 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.

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

KeyMeaning
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_backendThe 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 / urlsOne backend, or a pool. A urls entry is a URL or a {name, url} pair.
load_balancinground_robin or first_healthy. Defaults to round robin for two or more URLs.
base_pathA path prefix the backend expects on every request, such as /ext/bc/C/rpc.
headers, authenticationStatic headers, and one of bearer_token, plain_token, or username and password, sent on every request to the backend.
health_checkActive 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_modeeager validates a relay before serving it; optimistic serves first and validates afterwards. The global default is default_validation_mode: optimistic.
timeout_profilefast (30 s) or streaming (600 s, no response-header timeout), or a profile you define under timeout_profiles.
pool_profileA per-service connection pool: low, medium, high, or your own under pool_profiles.
max_body_size_bytesThe 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_RPCjsonrpc
WEBSOCKETwebsocket
RESTrest
GRPCgrpc
COMET_BFTcometbft

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.

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:

bash
pocket-relay-miner miner validate --config miner.yaml
pocket-relay-miner relayer validate --config relayer.yaml --check-stake

Each 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.

bash
docker compose -p pocket-supplier -f examples/docker-compose/docker-compose.yaml up -d

systemd. The repository ships unit files and an environment file in examples/host. Set GOMEMLIMIT and GOMAXPROCS in the environment file, then:

bash
sudo systemctl enable --now pocket-relay-miner-miner
sudo systemctl enable --now pocket-relay-miner-relayer

Check that the relayer is ready and each backend is healthy:

bash
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 service

8. 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:

bash
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:

bash
pocketd query proof list-claims --supplier-operator-address <operator address> --network=beta

To 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

SymptomCauseAction
Error: config is INVALIDA key is unknown, retired, or has a bad valueFix 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 evictsLoad config.redis.example.conf and set maxmemory.
cannot start without the node's networkThe miner cannot reach the node’s gRPCCheck query_node_grpc_url and grpc_insecure.
the node reports network "..."chain_id does not match the nodeSet the network’s chain_id.
/ready answers 503 no service factor manifestThe miner is not running yetStart the miner, then wait.
no memory limit foundNo GOMEMLIMIT and no container limitSet limits.
Relays answered 429 storage saturatedRedis is nearly fullRaise maxmemory, or let the miner drain.
Relays refused with no_local_signerThe relayer does not hold the key for the Supplier the relay namesAdd 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.