- How it works
- Node Eligibility Requirements
- Capacity Testing
- Getting started
- Pricing
- TLS options
- Manual setup
- Architecture
- Secret rotation
- Configuration
- Day-to-day operations
- CLI reference
- Troubleshooting
Customer → Gateway → Your Node
↓
Logs & Verification
↓
Validators score you
↓
Emissions paid per CU served
- You run your own node for your chosen chain, fronted by our nginx gateway that authenticates requests from the Blockmachine network
- The protocol gateway routes customer RPC requests to your node based on quality score and price
- Validators read gateway logs, verify correctness, and submit weights on-chain each epoch (~72 minutes)
- You earn emissions proportional to the CUs you served at your bid price
Eligibility is per chain and per node. A node failing on one chain does not affect your nodes on other chains.
| # | Requirement | What we check |
|---|---|---|
| U1 | Correct chain | eth_chainId (or the substrate genesis hash on TAO) matches the chain you registered the node on. |
| U2 | Honest node type | You declare archive or full. The declaration is verified (see below). Claiming archive without archive state is an eligibility failure, not a scoring nuance. |
| U3 | Reachable as registered | The exact endpoint URL you registered must accept WebSocket connections and answer JSON-RPC on it. If your endpoint lives under a path, register the URL with the path. We test what you registered, not what you meant. |
| U4 | At the tip | The node tracks the chain head. It must stay within 90 seconds of chain progress of the highest head we can see on that chain — see Staying at the tip below for the per-chain block allowance. A node that persistently lags the network tip is not serving the chain. |
| U5 | Standard responses | Correct JSON-RPC 2.0 shapes, including on errors. Proxies that return HTML error pages, rewrite error codes, or inject non-standard responses fail this. |
| U6 | Standard subscriptions | eth_subscribe(["newHeads"]) (or substrate equivalent) must work and actually deliver notifications. |
| U7 | Consistent identity | web3_clientVersion reports a real client honestly, matching the chain's accepted-client list (enforced at registration and re-checked continuously). Masking what you run is an eligibility failure. Known exception: Avalanche's coreth reports a bare version string with no client name, so Avalanche uses chain-ID + behaviour checks instead of a name match — an honest coreth node is never failed for its client string. |
| U8 | Continuous service | A required feature must be served continuously, not occasionally. A feature that works some of the time is treated as not served. (Brief reconnect blips are tolerated; flapping is not.) |
A node declared archive must serve blocks, state, logs, traces (where the chain requires them) and transaction lookups all the way back to genesis: any transaction in any block must be retrievable by its hash, so must its receipt, and so must the events that transaction emitted.
- Verified by random sampling: we ask for full blocks, state reads and (where the chain's clients support it) traces at randomly chosen historical heights across the entire chain history. There is no depth that is safe to prune.
- Geth-family clients (geth, bor and their forks) keep only a recent transaction index unless
told otherwise:
--history.transactions=0(geth 1.14 and later) or--txlookuplimit=0(older geth, bor) keeps every transaction retrievable by hash. Geth sets this itself under--gcmode=archive; check that your client and mode do the same. - Logs are pruned with the receipts. On every EVM chain the log index lives with the
receipt data, so the setting that decides how much receipt history your client keeps also
decides how far back
eth_getLogswill answer:--prune.receiptson reth,--prune.distanceon erigon,--gcmode=archiveon geth-family clients. Keeping block bodies is not enough. An archive node must answereth_getLogsfor any block in the chain's history, not only for recent ones. Applies from the eligibility run after this note is published. - The samples are never announced in advance and never reused, so there is nothing to warm and nothing to precompute. The only way to pass is to hold the data.
- An archive node must also serve everything a full node serves.
A node declared full must serve the chain head and the recent range correctly:
- Full blocks, state, logs and transaction lookups by hash (with their receipts) for at least
the last 100 blocks, except where a chain sets its own floor (below). Changes are
published here before they apply.
- Robinhood Chain: the last 5,000 blocks. Robinhood makes a block every 100 ms, so 100 blocks is ten seconds of history; 5,000 blocks (about eight minutes) is the floor a full node must serve. Applies from the eligibility run after this note is published.
- Polygon: the last 250 blocks. Polygon makes a block every 2 seconds, so 100 blocks is three minutes of history; 250 blocks (about eight minutes) is the floor a full node must serve. Applies from the eligibility run after this note is published.
- Ethereum: the last 10,000 blocks. This is not a new demand on your hardware: it is
what reth
--minimaland erigon--prune.distance=10064already retain by default, less a small safety margin, and it is the depth the gateway has always routed to non-archive ETH backends. Stating it here makes the published floor match what we actually ask for — previously the document said 100 while registration required 10,000, so a node built exactly to this page could be refused and not told why. Every ETH full node currently registered already meets it. Applies from the eligibility run after this note is published.
- Logs are included in that range. On every EVM chain
eth_getLogsmust answer for any block inside your node's floor above, and the log index is pruned with the receipt data (see the archive section for the client settings that govern it). Applies from the eligibility run after this note is published. - Your floor is what you must hold. It is not a cap on what you may be sent. Requests for blocks, logs, receipts or transactions older than your floor can reach a full node. You are never required to hold more than your floor, and not answering one of them is not an eligibility failure: eligibility is judged against your floor and nothing deeper. A node that keeps more history than its floor will be sent more of this work. Applies from the eligibility run after this note is published.
- All universal requirements above.
- Eligibility never asks a full node an archive-depth question, and a node is never
penalised for honestly being a full node. Declaring
fullwhile actually serving archive is fine; declaringarchivewhile serving full is not.
| Chain | Accepted client families | Mempool | Trace/debug | State proofs |
|---|---|---|---|---|
Ethereum (eth, chain-id 1) |
reth — run this. (erigon is also accepted, but only because we operate one erigon node ourselves for proof-based verification; miners should run reth.) | ✅ | ✅ | ❌ |
BSC (bsc, 56) |
geth (bsc-geth), reth (reth-bsc) | ✅ | ✅ | ✅ |
Base (base, 8453) |
reth | ❌ | ✅ | ✅ |
Optimism (optimism, 10) |
reth (op-reth) | ✅ | ✅ | ✅ |
Polygon (polygon, 137) |
bor | ✅ | ✅ | ✅ |
Avalanche C-Chain (avalanche, 43114) |
coreth / avalanchego (see U7 exception) | ❌ | ✅ | ✅ |
Scroll (scroll, 534352) |
geth (scroll-geth) | ✅ | ✅ | ✅ |
Mantle (mantle, 5000) |
geth (mantle op-geth) | ❌ | ✅ | ✅ |
Arbitrum One (arbitrum, 42161) |
nitro (a geth fork — either name is accepted in the version string) | ❌ | ✅ | ✅ |
Robinhood Chain (robinhood, 4663) |
nitro (as above) | ❌ | ✅ | ✅ |
TAO (Bittensor) (tao) |
subtensor | ✅ | ❌ | ✅ |
Why Base has no mempool requirement. Base's sequencer is private: public nodes cannot observe a meaningful pending-transaction feed, so there is nothing a node could serve and nothing we could fairly measure. Mempool is therefore not required on Base (same as the other chains marked ❌).
Why proofs are first-class. Proof-emitting methods (eth_getProof on EVM chains,
state_getReadProof on substrate) return answers that can be verified mathematically against
the block's state root — no comparison node, no trust. Where the chain's clients emit proofs,
we verify them, and a proof that does not check out is treated as a wrong answer, not a
formatting quirk.
Your node must stay within 90 seconds of chain height. We do not ask your node where the chain head is and measure back from there — a node that has stopped following the chain would pass that test forever, answering questions about its own frozen head. We compare your node's head against the highest head we can see across all nodes on that chain.
Ninety seconds is the same allowance everywhere. The block count differs only because the chains run at different speeds:
| Chain | Full node | Archive node |
|---|---|---|
Ethereum (eth) |
9 blocks | 55 blocks |
BSC (bsc) |
208 | 1,385 |
Base (base) |
46 | 305 |
Optimism (optimism) |
48 | 320 |
Polygon (polygon) |
63 | 415 |
Avalanche C-Chain (avalanche) |
93 | 615 |
Scroll (scroll) |
90 | 595 |
Mantle (mantle) |
48 | 315 |
Arbitrum One (arbitrum) |
378 | 2,515 |
Robinhood Chain (robinhood) |
972 | 6,475 |
TAO (Bittensor) (tao) |
8 | 50 |
- Archive nodes get ten minutes, not ninety seconds. An archive node that is behind is usually behind because it is doing expensive historical work, which is the job we are paying it for.
- The allowances are sized from the fastest we have ever seen each chain run. Several chains routinely beat their own published block time, so sizing from the documented figure would quietly make the rule stricter than ninety seconds. This way ninety seconds is a floor: when a chain is running slowly, the same block count buys you more time, never less.
- Falling behind is treated as temporary, and is retried before anything is flagged (see How testing works). A node catching up after a restart is not a node that has failed.
- These numbers change only when a chain changes its block speed, and any change is published here before it applies.
- Every node is re-tested regularly and automatically. Tests are randomly timed; there is no schedule to prepare for.
- A failure that looks transient (a timeout while your node is under heavy load, a brief disconnect) is retried before any flag is set. A node is never flagged on a single ambiguous observation.
- A failure that cannot be transient (wrong chain ID, a required method returning "method not found", pruned state on a declared archive) flags immediately.
- New nodes are tested first, with priority. A newly registered node must pass the full eligibility battery for its chain and type before it becomes eligible for traffic and incentive. You will not wait long: new-node tests jump the queue.
- Your node's current eligibility, the reason for any flag, when it was last tested, and when it will be re-tested are all visible in the miner API.
- No traffic routed to that node on that chain.
- No incentive earned by that node on that chain — a node that is not eligible is not in the payout set at all.
- Fix the issue, pass the next test, and both come back automatically.
Eligibility (see Node Eligibility Requirements) decides whether a node serves what the chain requires. Capacity testing decides how much traffic an eligible node deserves. It measures the hardware behind a node under a sudden heavy load and turns that into a multiplier on the node's routing weight. Operators who run genuinely strong hardware receive more requests and more incentive; operators who run many small boxes do not.
We take your node out of customer routing for a moment and fire a large burst of heavy requests at it all at once: hundreds of full block traces of expensive blocks, the kind of work paying customers actually ask for. Then we run the clock on every one of them:
- A correct answer costs the seconds it took to come back.
- An error, a malformed or incorrect answer, or anything slower than 10 seconds costs a flat 20 seconds.
The total is your score for the burst. Lower is better. We run three bursts on three different sets of blocks and take the mean.
Archive nodes are asked deep historical blocks, the same blocks for every archive node on the chain in that run, so results compare like for like. Full nodes are asked the newest blocks at the moment of the test, all full nodes on the chain at the same time, and "correct" is what the majority of well-formed answers agreed on.
Your multiplier is your score against the best score on the chain in that run:
multiplier = best score ÷ your score
The fastest node on the chain gets 1.0. A node twice as slow gets 0.5. A node that refuses or times out on half of a burst pays 20 seconds for every one of those requests and ends up near zero. The multiplier concentrates traffic on the nodes that can take a surge and answer it fast: a fleet of small, slow boxes earns by quality, not by numbers.
- The multiplier scales your node's routing weight on that chain. Higher multiplier, larger share of traffic and incentive.
- Scores are replaced by newer runs; they do not decay. A hardware upgrade shows up the next time you are measured.
- A run that could not measure your node fairly (a problem on our side, chain conditions) writes no score. An inconclusive run never harms you.
- Operators running nodes on several chains can expect those nodes to be tested at the same moment. Nodes that share one machine slow down together, and the scores will say so.
The burst size, the block selection, the exact request mix and the run schedule, and we change them. Every published detail becomes a thing to tune for instead of a thing to be. What we do publish are the properties that make the test fair:
- The same questions for every node in a run, fresh blocks every run, never the same block twice to the same node. Caches and canned answers do not help.
- The workload is genuinely heavy. Full traces of large blocks, not point reads a hot cache serves in a millisecond. A connection cap or a request limit in front of your node turns refused requests into 20-second penalties; the only way to score is to serve the burst.
- The test hits the endpoint you registered. There is no separate test endpoint to special-case, and the burst arrives with your node drained of customer traffic, so it measures your node and nothing else.
- Every node on a chain is measured on the same questions in the same run.
- Inconclusive measurements never lower a score.
- Scores, reasons and measurement times are visible to you.
- Methodology changes are published before they change anyone's routing. This section is that publication for the burst test.
You bring the node. Provisioning, syncing, disk sizing and snapshots are your responsibility — we provide the gateway that fronts your node and the CLI that registers and manages it. Your node must meet the eligibility requirements for its chain and declared type.
The CLI requires Python 3.10+ and can run anywhere — your laptop, a management server, or on the miner node itself. It's the control panel for registering nodes, managing secrets, and setting prices across your fleet without SSH-ing into each machine.
pip install blockmachineThe install script asks whether you're running on testnet or mainnet. For testnet, all bm CLI commands require the --testnet flag:
bm --testnet miner login
bm --testnet miner add --endpoint wss://... --alias my-node --secret '...' --price 0.01For mainnet (the default), use bm without the flag. The install script prints the correct commands for whichever network you choose.
Testnet requirement: You need a Bittensor hotkey registered as a miner on netuid 417 (the Blockmachine testnet subnet). Register your hotkey before running the install script:
btcli subnet register --netuid 417 --subtensor.network testbm miner login # mainnet
bm --testnet miner login # testnetThis uses a device authorization flow: the CLI displays a URL and code, then polls until you approve in a browser. The browser does not need to be on the same machine — you can run bm miner login on a headless server and open the URL on your phone or laptop. If a browser is available locally, it opens automatically.
SSH into your server and run the install script:
bash <(curl -sSL https://blockmachine.io/miner/install.sh)The script will:
- Install git and Docker if missing
- Clone this repository (or update it if re-running)
- Ask which chain your node serves
- Ask whether you have a domain name or are using an IP address
- Set up TLS (auto-renewing Let's Encrypt for domains, or self-signed for IPs)
- Generate a bearer token secret
- Start the gateway, pointed at your node's RPC port
- Print the registration commands to run from your local machine
At the end you'll see output like:
========================================
Miner is running!
========================================
Endpoint: wss://203.0.113.50
Chain: tao
Alias: tao-203-0-113-50
Secret: stored in /root/blockmachine-miner/.env
Now run these commands on your local machine:
bm miner login # or: bm --testnet miner login
bm miner add --endpoint wss://203.0.113.50 --alias tao-203-0-113-50 --secret '<secret from .env>' --price <usd-per-cu>
Using the CLI (wherever you installed it), register your node with the secret from the server's .env file (SECRET_V1):
bm miner add \
--endpoint wss://203.0.113.50 \
--alias my-node \
--secret '<SECRET_V1 from your server .env>' \
--price 0.01
# For testnet, prefix with --testnet:
bm --testnet miner add \
--endpoint wss://203.0.113.50 \
--alias my-node \
--secret '<SECRET_V1 from your server .env>' \
--price 0.01Or run bm miner add with no flags for interactive prompts.
What happens during registration:
- The CLI connects to the registry and creates your node entry
- For IP-based endpoints, the CLI fetches and pins your TLS certificate fingerprint (so the gateway can verify your identity via cert pinning)
- For domain endpoints, standard CA verification is used (no pinning needed)
- Your secret is hashed and stored — the gateway uses it to authenticate when routing requests to you
- Your price bid is recorded for the next epoch
Once your node is synced and the gateway is up, the network starts routing traffic to you. Verify everything is working:
bm miner test <alias> # Test TLS, health, and authenticated RPC
bm miner show # Check status and last seen timestampbm miner test runs three checks: TLS handshake on port 443, health endpoint on port 80, and an authenticated system_health RPC call. You can also test before registering with bm miner test --endpoint <url> --secret '<secret>'.
Once traffic is flowing, check your node's performance:
bm miner metrics [alias] # Quality score, latency, success rateEach miner exposes authenticated Prometheus metrics through the same HTTPS gateway:
curl -fsS \
-H "Authorization: Bearer <SECRET_V1 from your server .env>" \
https://203.0.113.50/metricsPrometheus scrape example:
scrape_configs:
- job_name: blockmachine-miners
metrics_path: /metrics
scheme: https
authorization:
type: Bearer
credentials: <SECRET_V1 from your server .env>
static_configs:
- targets:
- 203.0.113.50Metrics are operator telemetry for visibility and alerting. They are authenticated and useful for debugging sync, peer, disk, TLS, and version rollout issues, but they are not proof that a miner is honestly serving work.
The endpoint includes Blockmachine-curated health metrics, host CPU/memory/load, TLS certificate expiry, and the live deployed git SHA, and appends your chain client's native Prometheus output where the gateway can reach it.
You set a price in USD per Compute Unit (CU). A CU represents the normalized computational cost of serving a specific RPC method. Different methods cost different amounts of CU (a simple balance query costs less than a transaction trace).
The gateway routes traffic based on a combination of quality score and price — cheaper miners with good quality get more traffic. You only earn on successful responses (HTTP 200 with a JSON-RPC result).
Set or update your price:
bm miner price set --price 0.01 # USD per CU, effective next epoch
bm miner price show # Current price
bm miner price history # Price historySelect "Let's Encrypt" during install. A certbot container handles issuance and auto-renewal every 12 hours. Requires a domain name with a DNS A record pointing to your server, and port 80 reachable for the ACME challenge.
Generated automatically during install. Valid for 10 years. The CLI pins the certificate fingerprint during bm miner add so the gateway can verify your identity. No renewal needed.
Place cert.pem and key.pem in the ssl/ directory before running the install script. Select "no" when prompted about Let's Encrypt.
If you prefer to set things up yourself:
git clone https://github.com/taostat/blockmachine-miner.git
cd blockmachine-miner
# Generate a secret
SECRET=$(openssl rand -base64 32 | tr -d '=/+' | head -c 43)
# Create .env
cp .env.example .env
# Edit .env: set SECRET_V1=$SECRET, set DOMAIN if using Let's Encrypt
# Generate self-signed cert (skip if using Let's Encrypt or BYO cert)
IP=$(curl -s ifconfig.me)
mkdir -p ssl
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout ssl/key.pem -out ssl/cert.pem \
-subj "/CN=$IP" -addext "subjectAltName=IP:$IP"
# Start the gateway stack, pointed at your node's RPC port:
docker compose up -d
# For Let's Encrypt, add the TLS overlay:
# docker compose -f docker-compose.yml -f docker-compose.tls.yml up -dThen register from your local machine:
bm miner login
bm miner add --endpoint wss://$IP --alias my-node --secret "$SECRET" --price 0.01
# For testnet:
bm --testnet miner login
bm --testnet miner add --endpoint wss://$IP --alias my-node --secret "$SECRET" --price 0.01┌──────────────────────────────────────────────┐
│ Your Server │
│ │
│ ┌──────────┐ ┌───────────────────┐ │
│ │ nginx │ :443 │ your chain node │ │
│ │ gateway │─────▶│ (yours to run) │ │
│ │ │ :9944│ │ │
│ └──────────┘ └───────────────────┘ │
│ │ :80 (health + ACME) │ :30333 (p2p) │
│ │ │ │
│ │ /metrics │ │
│ ▼ │ │
│ ┌──────────┐ ┌────────┴─────────┐ │
│ │ metrics │─────▶│ node RPC + data │ │
│ │ │─────▶│ native :9615 │ │
│ │ │─────▶│ host /proc │ │
│ └──────────┘ └──────────────────┘ │
│ │
│ ┌──────────┐ │
│ │ certbot │ (optional) │
│ └──────────┘ │
└──────────────────────────────────────────────┘
- nginx gateway — Terminates TLS, authenticates requests via bearer token, proxies WebSocket and HTTP RPC to the chain node. Supports dual secrets for zero-downtime rotation. The eth gateway template routes by the
Upgradeheader so a single 443 endpoint serves both JSON-RPC andeth_subscribe. - chain node — yours: any accepted client for your chain, meeting the eligibility requirements for its declared type.
- metrics — Exposes
/metricsinternally; nginx publishes it athttps://<endpoint>/metricswith the same bearer auth as RPC. - certbot — Optional. Auto-renews Let's Encrypt certificates. Only used with domain-based setups.
Rotate your bearer token without dropping any traffic:
-
Set the new secret as
nextin the registry:bm miner secret set --secret '<new-secret>'
-
Add the new secret to your server and restart the gateway:
# Edit .env: set SECRET_V2=<new-secret> docker compose up -d gatewayThe gateway now accepts both the old and new secret.
-
Promote the new secret to
active:bm miner secret promote
The gateway now sends requests using the new secret.
-
Remove the old secret from your server:
# Edit .env: move SECRET_V2 value to SECRET_V1, clear SECRET_V2 docker compose up -d gateway
Environment variables in .env:
| Variable | Default | Description |
|---|---|---|
SECRET_V1 |
(required) | Primary bearer token |
SECRET_V2 |
(empty) | Secondary token for zero-downtime rotation |
DOMAIN |
(empty) | Domain for Let's Encrypt auto-renewal |
CHAIN |
tao |
Chain selection |
METRICS_PORT |
9100 |
Internal metrics exporter port |
SSL_CERT_PATH |
/etc/nginx/ssl/cert.pem |
TLS certificate path in container |
SSL_KEY_PATH |
/etc/nginx/ssl/key.pem |
TLS key path in container |
BM_MINER_GIT_SHA |
unknown |
Deployed repository commit exposed in metrics |
BM_MINER_GIT_BRANCH |
unknown |
Deployed repository branch exposed in metrics |
Chain-specific:
| Variable | Default | Used by | Description |
|---|---|---|---|
BACKEND_PORT |
9944 |
tao | Your node's RPC port |
BACKEND_HTTP_PORT |
8545 |
EVM chains | Your node's HTTP JSON-RPC port |
BACKEND_WS_PORT |
8546 |
EVM chains | Your node's WebSocket port |
bm miner show # Node status, endpoint, last seen
bm miner ls # List all your nodes
bm miner metrics [alias] # Quality score, latency, success rate
docker compose logs -f # Container logs
curl -sf http://localhost/health # Gateway health check
curl -fsS -H "Authorization: Bearer $SECRET_V1" https://<endpoint>/metricsPull the latest gateway config and images, then restart:
cd /root/blockmachine-miner && git pull && docker compose pull && docker compose up -dOr re-run the install script — it updates the repo automatically:
bash <(curl -sSL https://blockmachine.io/miner/install.sh)docker compose downAll commands below default to mainnet. Add --testnet after bm for testnet: bm --testnet miner ...
# Authentication
bm miner login # Authenticate with miner scopes
bm miner status # Check auth status
bm miner logout # Clear stored tokens
# Node management
bm miner add # Register a node (interactive)
bm miner use <alias> # Set active node for commands
bm miner ls # List all nodes
bm miner show [alias] # Show node details
bm miner update [alias] --endpoint ... # Change endpoint or alias
bm miner rm [alias] # Remove a node
# Secrets
bm miner secret set [alias] # Set bearer token secret
bm miner secret show [alias] # Show secret metadata
bm miner secret promote [alias] # Promote next secret to active
# Testing & metrics
bm miner test <alias> # Test TLS, health, and auth RPC
bm miner test --endpoint <url> --secret '<secret>' # Test before registering
bm miner metrics [alias] # Quality score, latency, success rate
# Pricing
bm miner price set [alias] --price ... # Set price per compute unit
bm miner price show [alias] # Show current price
bm miner price history [alias] # Show price history
When [alias] is omitted, the active node (set via bm miner use) is used.
Port 80 or 443 in use: Stop the process using the port (sudo lsof -i :443) and try again.
Gateway unhealthy: Your node may still be syncing — check your node's own logs.
Authentication errors: Run bm miner login to re-authenticate, then bm miner secret show to verify your secret is registered.
Node not receiving traffic: Check bm miner show — status should be active. If pending, the gateway hasn't connected yet (node may still be syncing). If unreachable, the gateway can't reach your endpoint — check firewall rules and that ports 443 is open.
TLS errors (self-signed): The gateway uses certificate pinning to verify your identity. If you regenerate your certificate, you need to re-register your node (bm miner rm then bm miner add) so the new fingerprint is captured.
TLS errors (Let's Encrypt): Check docker compose logs certbot. Ensure your domain's DNS A record points to your server and port 80 is reachable for the ACME challenge.
Firewall setup: If using ufw, allow the required ports:
sudo ufw allow 80/tcp && sudo ufw allow 443/tcp