Running a Provider
This guide walks through everything needed to join the Dispatch network as a provider — from staking GRT to receiving your first payment. By the end you will havedispatch-service running, registered on-chain, and serving live traffic.
What you need
| Requirement | Details |
|---|---|
| GRT | ≥ 10,000 GRT on Arbitrum One for the provision |
| ETH on Arbitrum | Small amount for gas (~0.005 ETH is plenty) |
| Ethereum node(s) | Full or archive node for each chain you want to serve |
| Server | Linux VPS with 2+ vCPUs, ≥ 4 GB RAM, SSD |
| PostgreSQL | For TAP receipt + RAV persistence (Docker Compose sets this up automatically) |
| Public HTTPS endpoint | Consumers and the gateway need to reach your dispatch-service |
1. Keys
You need two separate keys: Provider key — your on-chain identity. This is the address that holds the GRT provision in HorizonStaking and appears on-chain asserviceProvider. You call staking transactions with this key, but it does not need to be on the server.
Operator key — a hot key on the server. dispatch-service uses this key to sign response attestations and on-chain collect() transactions. It must be authorised in HorizonStaking to act on behalf of the provider address.
If you want to keep things simple, you can use the same key for both — isAuthorized always returns true when msg.sender == serviceProvider. For better security use separate keys.
Generate a fresh operator key if you don’t have one:
2. Stake on Horizon
All staking happens on Arbitrum One via the HorizonStaking contract at0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03.
2a. Stake GRT
If your GRT is in a wallet (not yet staked), approve and stake it:10000000000000000000000 with the amount in wei (1e18 per GRT). The minimum required by RPCDataService is 10,000 GRT (10000000000000000000000).
2b. Create a provision
A provision locks a portion of your staked GRT specifically forRPCDataService. This is what the contract checks when you register.
serviceProvider— your provider addressdataService—0xA983b18B8291F0c317Ba4Fe0dc0f7cc9373AF078(RPCDataService)tokens— amount in wei, minimum10000000000000000000000(10,000 GRT)maxVerifierCut—1000000(100% in PPM — the contract cannot slash, so this doesn’t matter in practice)thawingPeriod—1209600(14 days in seconds — the contract minimum)
2c. Authorise your operator key
If your provider key and operator key are different, authorise the operator:dataService—0xA983b18B8291F0c317Ba4Fe0dc0f7cc9373AF078(RPCDataService)operator— your operator address (derived from the hot key on your server)allowed—true
If you use the same key for both provider and operator, skip this step.
Verify the provision
tokens. It should be ≥ 10000000000000000000000.
3. Configure dispatch-service
Clone the repo and copy the example config:docker/config.toml:
Key settings explained
service_provider_address — your on-chain provider address. This is the address with the GRT provision, registered in RPCDataService. It does not need to hold any ETH or signing keys on the server.
operator_private_key — the hot key on this server. Its address must be authorised as an operator in HorizonStaking (step 2c). It signs TAP response attestations and broadcasts on-chain collect() transactions, so it needs a small amount of ETH on Arbitrum One for gas.
authorized_senders — list of gateway signer addresses allowed to send TAP receipts to this service. A gateway’s signer address is derived from the signer_private_key in its gateway.toml. If you’re using the public gateway, add its signer address here. Leave empty during initial setup to accept all senders.
aggregator_url — the internal URL of your dispatch-gateway. The service POSTs raw receipts here every 60 seconds; the gateway aggregates them into signed RAVs and returns them. If you’re running your own gateway in Docker Compose, this is http://dispatch-gateway:8080.
[collector] — when present, dispatch-service automatically calls RPCDataService.collect() on a timer, pulling GRT from the consumer’s escrow to your paymentsDestination. If you omit this section, collection does not happen and receipts accumulate without being redeemed.
4. Run with Docker Compose
Docker Compose is the recommended deployment. It runsdispatch-service, dispatch-gateway, and PostgreSQL together with health checks and automatic restarts.
(healthy) next to dispatch-service, dispatch-gateway, and postgres.
Check the service logs:
5. Register on-chain
Once the service is running, register your provider inRPCDataService and activate each chain you want to serve. The indexer agent handles this automatically.
Using the npm package
agent.config.json:
register(), startService() for each entry in services, and stopService() / deregister() on SIGTERM. It reconciles on-chain state against the config on every run — safe to run on a cron or as a persistent daemon.
Config fields
| Field | Description |
|---|---|
operatorPrivateKey | Hot key on your server — must be authorised as operator in HorizonStaking |
providerAddress | Your on-chain provider address (holds the GRT provision) |
endpoint | Public HTTPS base URL of your dispatch-service, reachable by gateways and consumers |
geoHash | Geohash of your server location — used for geographic routing. 4 characters is sufficient (e.g. u1hx for Amsterdam, dr4g for New York) |
paymentsDestination | Address that receives collected GRT. If omitted, defaults to providerAddress. Use a cold wallet here |
services | List of { chainId, tier } pairs — see Capability tiers below |
Verify registration
true.
(chainId, tier) pairs with active = true.
6. Expose your endpoint
Yourdispatch-service must be reachable at a public HTTPS URL. Port 7700 by default — put it behind nginx or Caddy with a TLS cert.
Minimal nginx config:
{"status":"ok"}.
7. Verify the payment loop
Make a test request through your service (with a valid TAP receipt) and confirm the full loop works. The easiest way is the smoke test binary:paymentsDestination wallet.
Capability tiers
Not all Ethereum nodes can answer all requests. A standard full node only keeps recent state (~128 blocks) — ask it for a balance at block 1,000,000 and it fails. A node without debug APIs enabled can’t servedebug_traceTransaction. If a gateway routed those requests blindly it would just get errors.
Capability tiers are how the network avoids that. Each tier describes a distinct infrastructure capability. You declare which tiers your node supports at registration time, and the gateway only routes requests to providers that can actually answer them.
| Tier | Value | What it serves | Node requirement |
|---|---|---|---|
| Standard | 0 | All standard JSON-RPC methods, recent ~128 blocks | Any full node |
| Archive | 1 | Historical state at any block number | Archive node (~10–20× more disk) |
| Debug/Trace | 2 | debug_* and trace_* methods | Full/archive node with debug APIs enabled (--http.api=debug,trace) |
| WebSocket | 3 | eth_subscribe, real-time event streams | Full node with a WebSocket endpoint |
One registration per (chain, tier) pair
Registration is granular. You callstartService(chainId, tier, endpoint) once for each capability you want to advertise — each call is a separate on-chain record. This means you can mix and match freely:
- Archive on Ethereum mainnet, Standard only on Arbitrum
- Debug on one chain, nothing on another
- WebSocket on all chains, Archive on none
services array in your indexer agent config maps directly to these calls:
startService calls, three on-chain records.
Stake is shared
Your staked GRT covers all tiers and all chains — there is no per-tier or per-chain stake splitting. The full provision applies regardless of how many (chain, tier) pairs you register for.Start with what your node supports
If you’re running a standard full node, register tier0 only. If it’s an archive node, add tier 1. Only enable tier 2 if you’ve explicitly enabled debug/trace APIs on your node — requests routed to you will fail otherwise and hurt your QoS score.
Supported chains
| Chain | Chain ID |
|---|---|
| Ethereum | 1 |
| Arbitrum One | 42161 |
| Optimism | 10 |
| Base | 8453 |
| Polygon | 137 |
| BNB Chain | 56 |
| Avalanche C-Chain | 43114 |
| zkSync Era | 324 |
| Linea | 59144 |
| Scroll | 534352 |
RPCDataService.addChain().
Managing your provision
Add more stake to your provision (if you want to serve more chains or increase your safety margin):deprovision to release the tokens back to idle stake, then unstake to return them to your wallet.
Update your payments destination (without re-registering):
stopService via the indexer agent by removing the entry from services in agent.config.json and re-running. Or call directly:
Deployed addresses (Arbitrum One)
| Contract | Address |
|---|---|
| HorizonStaking | 0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03 |
| GRT Token | 0x9623063377AD1B27544C965cCd7342f7EA7e88C7 |
| GraphTallyCollector | 0x8f69F5C07477Ac46FBc491B1E6D91E2bb0111A9e |
| PaymentsEscrow | 0xf6Fcc27aAf1fcD8B254498c9794451d82afC673E |
| GraphPayments | 0xb98a3D452E43e40C70F3c0B03C5c7B56A8B3b8CA |
| RPCDataService | 0xA983b18B8291F0c317Ba4Fe0dc0f7cc9373AF078 |