Deploy a Service
This guide takes you from a registered service ID to a supplier serving real relays. Budget a day: a few hours to build and test the backend against the rules below, an hour for staking and the RelayMiner, and a session or two of waiting for the network to pick your supplier up.
It assumes the service is already registered. If it is not, start with Register a Service. It also assumes you are the first supplier. A later supplier follows the same steps from Stake a Supplier onward, reading the service’s metadata card instead of writing it.
Let the tooling do the typing. The Pocket Service Builder skill for Claude lints your backend against the rules below and renders every file this guide describes: the stake configs, the HA RelayMiner configs and compose file, the reverse proxy, and the gateway snippets. On Windows, PSM4Win turns staking, deployment, and relay testing into buttons. See Tools for Service Builders.
Every relay a supplier serves is signed with the operator key. A lost operator key cannot be replaced on an existing supplier stake; the operator address is immutable. Back it up before staking.
Prerequisites
- A registered service ID with a metadata card. See Register a Service.
pocketdinstalled and two funded accounts: an owner that holds the stake and an operator that signs relays. Non-custodial staking, where these are different accounts, is recommended for production. See Supplier Staking for the owner-operator model and Accounts & Keys.- Enough POKT to stake. The supplier minimum is a governance parameter, currently 59,500 POKT on MainNet. Query it on the network you are staking on:
On Beta, request funds from the faucet listed on the Networks page. The operator account also needs a small working balance for claim and proof transaction fees.bash
pocketd query supplier params --network=beta - A public endpoint. Suppliers publish a URL that clients connect to. Use
https://on a hostname with a publicly trusted certificate, terminated by a reverse proxy you control, because the RelayMiner itself speaks plain HTTP. The protocol accepts plain HTTP, but gateways can refuse plain-HTTP or raw-IP URLs, and the Service Audit fails a service whose staked endpoint is either. - A Linux host with Docker (or systemd, if you prefer binaries) for the HA RelayMiner and Redis. See Minimal Server Specifications.
- Access to a Pocket full node, either your own or a public RPC and gRPC endpoint from the Networks page. The RelayMiner needs both.
Set Up Environment
export SERVICE_ID=<your-service-id>
export OWNER=<owner-key-name>
export OPERATOR=<operator-key-name>
export NETWORK=beta # or main
export TX_FLAGS="--network=$NETWORK --gas auto --gas-prices 1upokt --gas-adjustment 1.5"How Your Service Will Be Consumed
The protocol puts no constraints on the shape of a request or a response. A RelayMiner forwards whatever HTTP request arrives to your backend, signs whatever bytes the backend returns, status code and body included, and passes them back unaltered. A client that signs its own relays, such as an application using the SDK directly, could exchange any format with your backend.
In practice, almost all traffic reaches services through a gateway such as SAGE or PATH, and agents reach them through portals such as the Agentic Portal, which sits in front of a gateway. Gateways hold application keys for many users, select suppliers, retry failures, and score every supplier on the responses it returns. That scoring is what the rules in the next section are about. They are gateway rules rather than protocol rules, and a gateway operator can relax them per service, but a service designed for them works everywhere, and a service designed without them will not be routed to by the gateways that carry the network’s traffic.
Build the Backend
The backend is an ordinary HTTP server. It does not talk to the chain, verify signatures, or know what a session is; the RelayMiner does all of that. What the backend receives is a plain HTTP request with the method, path, query string, headers, and body the client sent, and what it returns is passed back to the client.
There is one catch. Gateways grade every response to decide which suppliers to trust, and the grade is applied to exactly the bytes your backend returned. A backend that ignores the rules below will work perfectly when you test it directly and will get its suppliers penalized in production, and a service that gets its own suppliers penalized is one nobody will keep supplying. Design to these rules from the first line of code.
Every Response Is a JSON Object
Gateways classify a response body by its first byte. A body that starts with { or [ is graded as a normal response. A body that starts with <html or <!DOCTYPE is treated as a proxy error page: the request is retried on another supplier and the supplier that answered is penalized and circuit-broken. Plain text, an empty body on a 200, or CSV get the same treatment.
So any output that is not itself JSON travels as a string inside a JSON object. The field names are your contract, not a network rule; what matters is that the body starts with {.
{
"content_type": "text/html",
"body": "<!DOCTYPE html><html>...</html>"
}Plain JSON string escaping is sufficient. Base64 is not needed and doubles the size. Put short metadata fields first and the large string last. A gateway operator can switch body grading off for a service, but you cannot count on every gateway doing so.
All Inputs Arrive in the Body
The relay format can carry headers, and the RelayMiner copies some of them onto the backend request, but you cannot rely on the caller’s headers arriving. SAGE forwards the HTTP method, the path, and the query string only; it drops every caller header and sets Content-Type: application/json on the request your backend sees. Self-signing clients forward most headers but strip credentials, and older gateways drop the query string as well. Design every endpoint to take its inputs from a JSON request body, including bulk data such as CSV, and use POST for anything with inputs.
No Caller Authentication
Relays are paid for by the application’s stake, not by a credential. Gateways and application-side relay clients strip Authorization, Api-Key, X-Api-Key, and Cookie headers before forwarding. If your backend needs a credential to reach something behind it, the HA RelayMiner can add static headers or a bearer token, plain token, or basic-auth credential to every request toward the backend; the caller never supplies one.
Status Codes
| Situation | Return | Why |
|---|---|---|
| Success | 200 with a JSON object | Graded as success. |
| Nothing to return | 204 with no body | The only status where an empty body is graded as correct. |
| Bad input | 400 or 422 with a JSON error object | Gateways treat a 4xx with a JSON body as the client’s mistake: delivered as is, not retried, no penalty. |
| Backend failure | 5xx | Retried elsewhere, the supplier is penalized, and the relay is not paid. Reserve for real failures. |
Never answer an expected condition with a 5xx, and never answer any error with HTML or plain text.
Avoid Error-Looking Words Early in a Success Body
Gateways also scan the first 2 KB of a body for substrings that indicate an upstream failure, among them timeout, bad gateway, service unavailable, gateway timeout, connection refused, and connection reset, and they retry on a match even inside a valid JSON object. Do not name fields timeout and do not echo those phrases in status messages.
Respond Within the Session
Gateways bound each relay attempt with a per-service timeout; the gateway configuration later in this guide uses 30 seconds, which matches the RelayMiner’s default fast timeout profile. A relay whose backend call finishes after the session ends, plus the grace period of 10 blocks, is not paid. Long-running work should return quickly with a handle rather than blocking. If the service genuinely streams, such as LLM token output over SSE, put it on the RelayMiner’s streaming timeout profile and say so in the card’s serving.notes, because every gateway in the path has to allow for it too.
Serve Identity Encoding
The RelayMiner asks your backend for Accept-Encoding: identity and signs the exact bytes it gets back. Do not gzip responses.
Provide the Probe Endpoints
Your metadata card’s serving.healthcheck names an identity endpoint, a readiness endpoint, and a cheap functional request. Implement them exactly as the card describes; suppliers, the RelayMiner’s own health checks, gateways, and the Service Audit all run them. The identity endpoint must return the service ID itself, because the audit checks that the identity probe pins the response to this service.
GET /v1/version → {"service": "<service-id>", "version": "1.2.0"}
GET /v1/health → {"status": "ok"}
POST /v1/<resource> with a minimal body → a deterministic answerTest the Backend Directly
Before involving the network, run the card’s health checks against the backend with curl, and confirm that every response, including error responses, starts with {. The Service Builder skill’s backend linter does this for you against a running backend and reports every rule it breaks.
Run the HA RelayMiner
The RelayMiner is the software that sits in front of your backend, verifies each signed relay, forwards it to the backend, signs the response, and turns what it served into claims and proofs so the supplier gets paid. Use the HA RelayMiner, pocket-relay-miner. It is the only RelayMiner recommended for new services: it serves REST, JSON-RPC, WebSocket, gRPC, streaming (SSE), and CometBFT backends; actively health-checks each backend; and scales from one machine to many replicas without changing the supplier stake.
The single-process RelayMiner built into pocketd (pocketd relayminer) is legacy. It struggles with REST services and is no longer recommended for supplying a service. Existing deployments can move to the HA RelayMiner without touching the supplier stake.
One deployment is one binary run as two processes, plus one Redis that both share:
gateways ──► relayer :8080 ──► your backend
│
▼
Redis (served relays, claim trees, the stake meter)
▲
│
miner ──► Pocket chain (claims, proofs)- Relayer. Stateless. Verifies each relay’s ring signature and session, charges it against the application’s stake, forwards it to the backend for its service, signs the response, and queues the relay in Redis. Run one or several behind a load balancer.
- Miner. Reads the queued relays, builds one claim per session and supplier, and submits the claim and then the proof in their on-chain windows. Run a second as a standby; miners elect a leader through Redis.
- Redis. Holds every relay until it is claimed and proved. It is not a cache: a lost key is a claim that cannot be proved, so it must never evict.
The repository’s deployment guide has step-by-step runbooks for Docker Compose and for a host with systemd, a troubleshooting guide, and ready Grafana dashboards. The rest of this section is the part that is specific to supplying a REST service.
Requirements That Stop It Starting
- The same version everywhere. The relayer and the miner must run the same release, for example
ghcr.io/pokt-network/pocket-relay-miner:v0.1.0. Check the releases page for the current one and read its notes before upgrading. - Redis 8.10 or newer, with
maxmemoryset andmaxmemory-policy noeviction. Both processes refuse to start otherwise. The repository’sconfig.redis.example.confis a starting point, and Redis data must be on a persistent volume. - A CPU and memory budget. Set
GOMAXPROCSandGOMEMLIMIT, or run each process in a container with CPU and memory limits. Without them each process sizes itself against the whole host. - The miner’s
chain_idandblock_time_secondsmatch the network:pocket-lego-testnetand30on Beta TestNet,pocketand60on MainNet.
Supplier Keys
Both processes sign with the supplier’s operator key, read from a keys file or a pocketd keyring. The keys file is the simplest start:
# supplier-keys.yaml, mode 0600; never commit it
keys:
- "<operator private key, 64 hex characters>"Export the key from the keyring you created it in:
pocketd keys export $OPERATOR --unarmored-hex --unsafeFor production, point the keys.keyring block at the keyring directly instead, with the passphrase in a file or environment variable. See SUPPLIER_KEYS.md in the repository.
Relayer Configuration
# relayer.yaml
listen_addr: "0.0.0.0:8080" # the reverse proxy forwards here
redis:
url: "redis://redis: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
keys:
keys_file: "/keys/supplier-keys.yaml"
services:
<your-service-id>:
validation_mode: eager
timeout_profile: fast # or streaming, for long or SSE responses
max_body_size_bytes: 20971520
default_backend: rest
backends:
rest:
url: "http://backend:8080" # your backend
health_check:
enabled: true
endpoint: "/v1/health"
expected_body: '"ok"'
interval_seconds: 10
timeout_seconds: 5
unhealthy_threshold: 3
healthy_threshold: 2
health_check:
enabled: true
addr: "0.0.0.0:8081"
metrics:
enabled: true
addr: "0.0.0.0:9090"
logging:
level: "info"
format: "json"Points that matter for a REST service:
- The backend key is the transport. It must be one of
rest,jsonrpc,websocket,grpc, orcometbft, and it maps to the card’srpc_types[].type:RESTin the card isresthere,JSON_RPCisjsonrpc,COMET_BFTiscometbft. A relay is routed by exact transport match with no fallback, so an unknown key is rejected at startup. - Set
default_backend: rest. It is the backend a relay goes to when it does not name a transport, and it defaults tojsonrpc. Leaving it unset on a REST-only service sends those relays nowhere. - The client’s path is appended to the backend URL. With
url: http://backend:8080/api, a request for/v1/chartreaches the backend at/api/v1/chart. Abase_pathon the backend overrides the URL’s path, and a request that already starts with the prefix is not prefixed twice. - Turn on the active health check. Without one, the relayer learns a backend is down only by failing real relays. Point it at the card’s readiness endpoint;
expected_bodyis a substring the response must contain. Leave it off onwebsocketandgrpcbackends, where the HTTP probe always fails. - Every backend request carries
Pocket-Supplier,Pocket-Service, andPocket-Applicationheaders. They are the only per-request identity your backend will ever see, and they are useful for logging and per-application rate limiting. - Backend credentials go on the backend. A
headersmap and anauthenticationblock (bearer_token,plain_token, orusernameandpassword) under the backend are sent on every request to it.
The relayer reads its configuration once, at startup. Restart it after every change, and add one entry under services for each service the supplier is staked for.
Miner Configuration
# miner.yaml
redis:
url: "redis://redis: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: "/keys/supplier-keys.yaml" # the same keys as the relayer
block_time_seconds: 30 # 60 on MainNet
metrics:
enabled: true
addr: "0.0.0.0:9092"
logging:
level: "info"
format: "json"The public Sauron endpoints are fine for a first deployment. The miner submits the claims and proofs you are paid for, so a production supplier should point it at a full node it controls.
Validate, Then Start
Validate both configurations with the exact files you will start. --check-stake also lists any service the supplier is staked for that has no backend:
pocket-relay-miner miner validate --config miner.yaml
pocket-relay-miner relayer validate --config relayer.yaml --check-stakeStart Redis, then the miner, then the relayer. The repository’s Docker Compose example wires all three together, with health checks, memory limits, and the startup order, and is the fastest way to run it:
docker compose -p pocket-supplier up -dConfirm the relayer is ready and can reach your backend:
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8081/ready # 200 once it can serve relays
curl -s http://localhost:8081/ready/$SERVICE_ID # each backend's health state/ready answers 503 until the miner is running, so a relayer that never turns ready usually means the miner is not up.
Put a Reverse Proxy in Front
The relayer listens on plain HTTP. Terminate TLS on a reverse proxy such as Caddy, nginx, or your cloud load balancer, and forward to the relayer’s listen_addr. The proxy’s public https:// hostname is what you will stake as the supplier’s endpoint. Do not add authentication at the proxy; every request is already signed.
Stake a Supplier
The stake declares which service you serve and where. The full field reference and the owner-operator model are in Supplier Staking. The stake amount must be at least the live min_stake, which on MainNet is currently 59500000000upokt.
# supplier_stake_config.yaml
owner_address: <owner pokt1... address>
operator_address: <operator pokt1... address>
stake_amount: <amount>upokt # at least the live min_stake
default_rev_share_percent:
<owner pokt1... address>: 100
services:
- service_id: <your-service-id>
endpoints:
- publicly_exposed_url: https://relay.example.org
rpc_type: RESTrpc_type must match the card. Gateways select supplier endpoints by transport. A card that declares REST and a stake that declares JSON_RPC means REST clients never see your endpoint, and the Service Audit fails the service. Use exactly the type the card’s rpc_types lists.
A stake lists every service the supplier serves. To add a service later, stake again with the full list, and add the service to the relayer configuration at the same time.
Before staking, send any transaction from the operator account, for example a tiny self-transfer, so its public key is recorded on-chain. Gateways reject responses from an operator whose key they cannot look up.
Confirm it landed before you stake. Once the transaction is in a block, the account’s record carries a public_key; until then the field is missing:
pocketd query auth account <operator-address> --network=$NETWORK -o jsonpocketd tx supplier stake-supplier \
--config ./supplier_stake_config.yaml \
--from $OPERATOR \
$TX_FLAGSVerify:
pocketd query supplier show-supplier <operator address> --network=$NETWORKThe service configuration becomes active at the start of the next session. A session is 20 blocks, about 20 minutes on MainNet. Beta TestNet’s blocks are shorter, so its sessions are shorter too; see the Networks page.
Test End to End
Testing needs an application, because relays are paid for by an application stake. Stake a small one for your own service on Beta. See Application Staking for the details. The application minimum is a governance parameter, currently 1,000 POKT on MainNet; query it on Beta with pocketd query application params --network=beta. The short form is:
# app_stake_config.yaml
stake_amount: <amount>upokt # at least the live application min_stake
service_ids:
- <your-service-id> # exactly onepocketd tx application stake-application --config ./app_stake_config.yaml --from <app-key> $TX_FLAGSWait for the next session, then send relays with pocket-ap, a command-line relay client from the pocket-ap repository. It signs with the application key directly; no gateway and no delegation are needed, because an application is always a member of its own signing ring. On Windows, PSM4Win’s Test step does the same from a button.
go install github.com/pokt-network/pocket-ap/cmd/pocket-ap@latest # or: brew install pokt-network/tap/pocket-ap# pocket-ap.yaml
network: beta
listeners:
- addr: 127.0.0.1:8550
service_id: <your-service-id>
rpc_type: rest
apps: [] # key comes from the environmentPOCKET_APP_PRIVATE_KEY=<app key hex> pocket-ap call \
--config ./pocket-ap.yaml \
--service <your-service-id> --rpc-type rest \
-X POST --path /v1/<resource> \
-d '{"...": "..."}' -vIts --compare <url> flag sends the same request straight to your backend and diffs the two answers, which is the fastest way to catch a path-prefix or content-type mistake.
Then confirm the relay was accounted for. Within a session or two of serving relays you should see a claim from your operator:
pocketd query proof list-claims --supplier-operator-address <operator address> --network=$NETWORKKeep going until the claim settles. A settled claim is the only on-chain proof that a relay went through the protocol, reached your backend, and was paid, and the Service Audit requires one on the network you submit for.
To test the path most users will take, run a gateway locally. SAGE is the current gateway implementation; the configuration it needs for your service is in the next section.
Give Gateway Operators What They Need
Today each gateway is configured for your service by hand, and if it is not, a REST request to it is refused before a session is even looked up. Card-driven configuration is on the near-term roadmap, so keep the card and this snippet in sync, and publish the snippet in your service documentation so an operator can copy it.
The SAGE service entry:
gateway_config:
services:
- id: <your-service-id>
type: passthrough
rpc_types: ["rest"]
timeout_config:
relay_timeout: 30s
active_health_checks:
local:
- service_id: <your-service-id>
enabled: true
checks:
- name: version
type: rest
method: GET
path: /v1/version
expected_status_code: 200
reputation_signal: critical_error
timeout: 5s
- name: health
type: rest
method: GET
path: /v1/health
expected_status_code: 200
reputation_signal: major_error
timeout: 5sThe health checks mirror your card’s serving.healthcheck. Keep the two in sync. Do not add sync_check or sync_allowance to a non-blockchain service; those assume a block height and will mark every healthy supplier as failing.
Minimal Server Specifications
Four components run on the supplier side. They can share one machine at first.
| Component | Minimum | Comfortable | Notes |
|---|---|---|---|
| Service backend | Whatever your service needs | Size it for concurrent requests, not average load. Gateways hedge and retry, so bursts are normal. | |
| Relayer | 2 vCPU, 2 GB RAM | 4 vCPU, 8 GB RAM | CPU scales with relays per second. Stateless; add replicas behind a load balancer for more. |
| Miner | 2 vCPU, 4 GB RAM | 2 vCPU, 8 GB RAM | Memory grows with the number of suppliers and the size of relays in flight. |
| Redis | 2 vCPU, 4 GB RAM, persistent disk | 5 vCPU, 16 GB RAM | Size it for peak relays per second × average request size × about 40 blocks. Never expose its port. |
| Full node | 4 vCPU, 16 GB RAM, 200 GB SSD | 6 vCPU, 32 GB RAM, 420 GB SSD | Optional. A public RPC and gRPC endpoint from the Networks page works for a first deployment, but a production RelayMiner should not depend on infrastructure it does not control. |
The minimums are the limits in the repository’s Docker Compose example, which fits a few suppliers with ordinary traffic; the comfortable column is what its published capacity report used at about 2,400 relays per second. The benchmarks explain how to size for your own load. Linux on x86_64 or ARM64 is the supported environment for the RelayMiner and full node.
A new service with light traffic runs on a much smaller server than these, as long as every part has a memory limit. Supplier Servers covers that case: sizing memory between the RelayMiner and your backends, swap, and preparing the server.
Beyond hardware:
- Persistent disk for Redis data and the keys file or keyring.
- A public endpoint, on HTTPS with a publicly trusted certificate on a hostname.
- Operator balance kept above a few POKT at all times. Claims and proofs are transactions; an operator that cannot pay the fee forfeits the session’s earnings.
- Metrics scraped. The relayer exposes Prometheus metrics on
:9090and the miner on:9092. The repository ships Grafana dashboards and a triage order for incidents. See also Monitoring.
Going to MainNet
- Register the service on MainNet if you have not yet. See Register a Service.
- Re-query
min_stake,add_service_fee, and the shared pricing parameters with--network=main. - Repoint
pocket_nodein both configurations at MainNet, set the miner’schain_id: pocketandblock_time_seconds: 60, and validate again. Every other line is unchanged. - Stake, wait a session, run the card’s health checks through
pocket-ap, and wait for the first claim to settle. - Run the Service Audit against MainNet with your operator address and fix everything it fails.
- Publish the gateway configuration and the card’s
docsURL. To have agents find the service, list it on the Agentic Portal. A supplier no gateway routes to earns nothing.
Related Pages
- Register a Service — the on-chain record and metadata card this guide builds on
- Supplier Servers — sizing, preparing, and running the server behind a supplier
- List Your Service — the Agentic Portal’s listing criteria and the Service Audit
- Supplier Staking — full stake configuration and the owner-operator model
- pocket-relay-miner — the HA RelayMiner repository, with deployment runbooks, every configuration key, and troubleshooting
- Application Staking — staking the test application
- Monitoring — metrics and alerting
- Rewards & Economics — how supplier income is calculated