MDK Logo

Run a mining site end to end

[⏱️ ~3 min] From git clone to a live site with multiple Workers, mock hardware, and a browser dashboard

If Kernel, Gateway, Worker, manager, or thing are unfamiliar, terminology defines them.

Overview

This tutorial runs the Starter site example end to end: Whatsminer, Antminer, and SealMiner Workers, Ocean, EMCD, SpiderPool, and Foundry pool Workers, and a SATEC powermeter Worker, each backed by a mock that speaks the real wire protocol, plus a Gateway HTTP API and a React dashboard, all supervised by PM2.

What you'll have at the end:

  • Mock miners, four mock pool APIs, and a mock powermeter, each driven by the real Worker driver code against a localhost mock instead of hardware
  • A Gateway API on :3000 serving /site/overview, /site/history, /site/miners/:id/command, and /site/miners/:id/pools
  • A React dashboard on :3000 with Dashboard, Containers, Monitoring, Pools, and Control pages
  • One MCP surface on :3101 exposing the site as tools for AI agents, served by a standalone MCP process: a hand-authored, agent-contract tool set alongside the site app's own routes, read off the same route table the Gateway forwards

Every component above runs as its own PM2-supervised OS process. Kernel discovers the Workers through a shared local directory rather than a DHT topic, and the Gateway and MCP server find Kernel through its well-known key file.

Prerequisites

Install the example

1.1 Clone the repo

git clone git@github.com:tetherto/mdk.git
cd mdk

1.2 Run setup

cd examples/mvp-site
npm run setup
npm run setup:config

setup installs backend/core, backend/workers, the UI workspace devkit packages, and this example's own dependencies. setup:config copies the committed *.json.example config files into place without overwriting any that already exist.

The script walks several workspaces; first run takes 1-2 minutes.

Start the site

npm start

start.js generates deploy/ecosystem.config.js from config/site.deploy.json, starts the PM2 apps, then exits — PM2 itself keeps the processes running in the background.

pm2 list      # mocks, mocks-antminer, mocks-ocean, mocks-satec, mocks-emcd, mocks-spiderpool, mocks-foundry, kernel, worker, worker-antminer, worker-ocean, worker-satec, worker-emcd, worker-spiderpool, worker-foundry, app-site, gateway, mcp

PM2 identifies processes by role name and shares one daemon across every project on the machine. Starting this site while another site that uses the same roles (mocks, kernel, gateway, mcp) is running restarts that site's processes instead of starting new ones, and stopping either site removes both. Stop the other site first, with npm run stop:pm2 from its directory.

Wait for every app to show online, then open http://localhost:3000/ in a browser and sign in as one of the example's demo users. The dashboard shows live hashrate, power, and per-device status.

PM2 showing every app online means the processes started, not that the site has fully settled. The pool Workers tick their mock fetch/save cycle every 10 seconds, SpiderPool spaces its API requests one second apart, and Kernel's local-discovery scan runs every 4 seconds, so /site/overview can report fewer than 4 pools for 15-20 seconds after pm2 list goes green. Retry the command below if the first response comes back short.

Verify via the API. Every route needs a signed-in user, so sign in first and send the access token. viewer is the read-only demo user; any of the others works too.

TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
  -H 'content-type: application/json' \
  -d '{"username":"viewer","password":"mdk-site-demo"}' | jq -er .accessToken)

curl -s -H "Authorization: Bearer $TOKEN" \
  http://localhost:3000/site/overview | jq '{miners: (.miners|length), pools: (.pools|length), powermeters: (.powermeters|length)}'

Expected output:

{
  "miners": 10,
  "pools": 4,
  "powermeters": 1
}

config/devices.json seeds five Whatsminer miners, three Antminer miners, two SealMiner miners, and one powermeter by default. Add or remove entries there to resize the fleet; configuring devices describes the format. The four pools aren't seeded devices: each pool Worker reports one account from its section of config/site.deploy.json.

(Optional) Explore the UI pages

The Dashboard, Containers, Monitoring, Pools, and Control pages are all served from ui/dist via the Gateway on the same :3000 port. The Pools page lists the Ocean, EMCD, SpiderPool, and Foundry accounts side by side; the Monitoring page charts the SATEC meter's power series over time. UI pages describes what each one shows.

(Optional) Report a real EMCD, SpiderPool, or Foundry account

The EMCD, SpiderPool, and Foundry Workers read their API credentials from the environment, never from config/site.deploy.json. With the variables unset, each Worker logs a warning and authenticates with its package's public mock credentials, which only the bundled mock accepts.

PoolEnvironment variablesAccounts setting
EMCDEMCD_API_KEY, EMCD_API_SECRETemcd.pool.accounts: subaccount IDs, empty for master
SpiderPoolSPIDERPOOL_ACCESS_KEY, SPIDERPOOL_PRIVATE_KEYspiderpool.pool.accounts: subaccount names
FoundryFOUNDRY_API_KEYfoundry.pool.accounts: sub-account names, required

To switch one pool to the live API, set its variables in the shell you run npm start from, then in config/site.deploy.json remove that pool's pool.apiUrl so the Worker uses its package default, set pool.accounts, and remove its mocks-emcd, mocks-spiderpool, or mocks-foundry entry from pm2.roles. Run npm run stop:pm2 and npm start to apply it. The pool config reference covers every setting.

Ask a coding agent about the fleet

The mcp process started in step 2 serves the site as tools on :3101. It is the same standalone server that mdk run mcp starts for a project scaffolded with the CLI; here the example launches it as a PM2 process.

Connect your coding agent to the server. .mcp.json.example holds the entry. For an agent that reads a project-level .mcp.json (such as Claude Code), copy it and start the agent from the example directory:

cd examples/mvp-site
cp .mcp.json.example .mcp.json
claude

Accept the mdk-site-agent-tools server if prompted, then confirm it shows as connected.

For any other MCP client, add http://127.0.0.1:3101/mcp as an HTTP server in its MCP settings.

Ask these three questions in order:

PromptTool the agent callsWhat comes back
How is the site doing right now?summarize_site8 Workers and 15 devices (10 miners, 4 pools, 1 powermeter), all online
Which three miners are running the hottest?rank_devicesAM-001, AM-002, and AM-003 at 97 °C
What power mode is WM-001 in, and how hot is it?get_devicenormal mode, 92.9 °C

The wording and figures may vary slightly depending on your model and mock seeds, but your agent should call these tools and report figures like those in the table. The mock miners of one model report the same readings, so ties in the ranking are expected.

The MCP server withholds write tools by default, so an agent can read the fleet but cannot change it: asking it to reboot a miner finds no tool to call. The example's act_device tool is declared but not served. See tool exposure to enable writes, and the conversational operator agent for a human approval gate on them.

The server carries two tool sources:

  • A hand-authored tool set with agent-contract metadata (backend/mcp-plugins/site/mcp-plugin.json) giving an agent summary-first, closed-vocabulary tools (summarize_site, count_devices, list_devices, get_device, rank_devices) instead of raw route exports
  • The site app's own /site/* routes, read off the same route table the Gateway forwards and converted into tools

Point an MCP client at that URL and it can query the fleet or, with the agent-contract tools, act on it with operator approval. This local site admits its own processes through the trusted-keys directory on this machine; a hardened Kernel restricts the MCP connection through Kernel's caller allowlist.

Stop the site

npm run stop:pm2

This stops and removes every PM2-managed process for this site: mocks, Workers, Kernel, the site app, Gateway, and the MCP server.

The PM2 daemon itself (pm2 list still shows a God Daemon process) stays running in the background after this — that's expected, since PM2 manages processes across every project on the machine, not just this one. If it ever becomes unresponsive, see stale PM2 processes for pm2 kill, which stops PM2 itself, not just this site.

What just happened

  1. Setup installed backend/core, backend/workers, the MDK UI devkit, and this example, then setup:config seeded its local config from the committed *.example files.
  2. Mock hardware: PM2's mocks, mocks-antminer, mocks-ocean, mocks-emcd, mocks-spiderpool, mocks-foundry, and mocks-satec roles each started a mock server (Whatsminer and Antminer miners, the Ocean, EMCD, SpiderPool, and Foundry pool APIs, and a SATEC powermeter) speaking the real wire protocol, so the Worker drivers run their true connect, collect, and command paths against them.
  3. Kernel: the kernel role started the orchestration layer in local-discovery mode, watching a shared directory for Workers to publish their RPC keys to.
  4. Workers: the worker, worker-antminer, worker-ocean, worker-emcd, worker-spiderpool, worker-foundry, and worker-satec roles each dispatched to that family's boot function to construct a WorkerRuntime and publish its RPC key for the Kernel to discover. The miner and powermeter Workers seed their devices from config/devices.json. Each pool Worker is one logical device that polls its pool's API. The EMCD and SpiderPool Workers come from external git packages, booted by backend/pool-adapter.js.
  5. App and Gateway: the app-site role ran the site app (backend/gateway-plugins/site) in its own app runtime, and the gateway role mounted that app's routes at / and the built UI from ui/dist, then opened the HTTP server on :3000, forwarding each /site/* request to the app. The app aggregates data across the six Workers through mdkClient.
  6. MCP: the mcp role started the standalone MCP process on :3101, serving both tool sources — the hand-authored agent-contract tool set and the site app's own routes, derived from the same route table the Gateway forwards.

Resetting state

State (Kernel key, Worker seeds, device registry) persists in .site-data/. If you change config/devices.json after the first run, reset it before restarting — seed devices are only registered once, on an empty store.

Next steps

On this page