Skip to main content

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 have dispatch-service running, registered on-chain, and serving live traffic.

What you need


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 as serviceProvider. 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:
Note the address — you will need it in step 2 and again in the service config.

2. Stake on Horizon

All staking happens on Arbitrum One via the HorizonStaking contract at 0x00669A4CF01450B64E8A2A20E9b1FCB71E61eF03.

2a. Stake GRT

If your GRT is in a wallet (not yet staked), approve and stake it:
Replace the amount with your desired stake in wei (1e18 per GRT). The minimum required by RPCDataService is 555 GRT (555000000000000000000).

2b. Create a provision

A provision locks a portion of your staked GRT specifically for RPCDataService. This is what the contract checks when you register.
Arguments:
  • serviceProvider — your provider address
  • dataService0x7101d5c1a5c89c3647f5118da118e56c023ba0b9 (RPCDataService)
  • tokens — amount in wei, minimum 555000000000000000000 (555 GRT)
  • maxVerifierCut1000000 (100% in PPM — the contract cannot slash, so this doesn’t matter in practice)
  • thawingPeriod1209600 (14 days in seconds — the contract minimum)

2c. Authorise your operator key

If your provider key and operator key are different, authorise the operator:
Arguments:
  • dataService0x7101d5c1a5c89c3647f5118da118e56c023ba0b9 (RPCDataService)
  • operator — your operator address (derived from the hot key on your server)
  • allowedtrue
If you use the same key for both provider and operator, skip this step.

Verify the provision

The first number is tokens. It should be ≥ 555000000000000000000 (555 GRT).

3. Configure dispatch-service

Clone the repo and copy the example config:
Edit 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. If you’re routing traffic through the public Dispatch gateway, add its signer address:
Leave empty ([]) during initial setup to accept receipts from any sender — tighten this once you’ve confirmed the payment loop is working. aggregator_url — the URL the service POSTs raw receipts to every 60 seconds for RAV aggregation. Two options:
  • Using the public Dispatch gateway (most providers): aggregator_url = "https://gateway.lodestar-dashboard.com"
  • Running your own gateway (Docker Compose): aggregator_url = "http://dispatch-gateway:8080"
The gateway verifies that each receipt was signed by itself, aggregates them into a signed RAV, and returns it for on-chain collection. [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 runs dispatch-service, dispatch-gateway, and PostgreSQL together with health checks and automatic restarts.
Check that all three containers are healthy:
You should see (healthy) next to dispatch-service, dispatch-gateway, and postgres. Check the service logs:
Expected output on startup:

5. Register on-chain

Once the service is running, register your provider in RPCDataService and activate each chain you want to serve. The indexer agent handles this automatically.

Using the npm package

Create agent.config.json:
Run it:
The agent calls 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

Verify registration

Should return true.
Should show your registered (chainId, tier) pairs with active = true.

6. Expose your endpoint

Your dispatch-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:
Test it:
Should return {"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:
All 5 checks should pass. After 60 seconds, check service logs for:
After an hour (or force a collect manually):
GRT lands in your 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 serve debug_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.

One registration per (chain, tier) pair

Registration is granular. You call startService(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
The services array in your indexer agent config maps directly to these calls:
This registers Standard and Archive on Ethereum mainnet, and Standard only on Arbitrum One. Three 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 tier 0 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

Chains are governance-controlled. New chains are added via RPCDataService.addChain().

Managing your provision

Add more stake to your provision (if you want to serve more chains or increase your safety margin):
Start thawing (to eventually remove GRT from the provision):
After the 14-day thawing period, call deprovision to release the tokens back to idle stake, then unstake to return them to your wallet. Update your payments destination (without re-registering):
Stop serving a chain (without deregistering): Send 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)