See a plugin serve your data
[⏱️ <2 min] Run one command and watch a Gateway plugin answer an HTTP request with live telemetry aggregated across two devices
If Kernel, Gateway, Worker, or plugin are unfamiliar, read terminology first.
Overview
From 0.6.0 the Gateway serves no data routes of its own. It is a plugin host, so every endpoint your application reads comes from a plugin you mount. This tutorial runs the plugin-authoring example so you can watch that happen once before writing your own.
What you'll have at the end:
- Two mock miners on
127.0.0.1:15101and:15102, reporting different hashrate and power figures - One
WorkerRuntimehosting both devices behind a single RPC channel to the Kernel - A Gateway on
:3210with one mounted plugin servingGET /api/fleet/summary - The JSON body of that request printed to your terminal, with the two devices aggregated into fleet totals
The example verifies its own arithmetic and exits, so there is nothing to clean up and nothing left running.
Prerequisites
- Node.js >=24 (LTS)
- npm >=11
Clone and install
git clone git@github.com:tetherto/mdk.git
cd mdk
npm installRun the example
node examples/backend/mdk-plugin-e2e/run.jsThe whole run takes a few seconds. The interesting part is the last block:
GET /api/fleet/summary → {
"deviceCount": 2,
"totalHashrateThs": 240,
"totalPowerW": 6200,
"devices": [
{ "deviceId": "SIM-001", "hashrateThs": 100, "powerW": 3000 },
{ "deviceId": "SIM-002", "hashrateThs": 140, "powerW": 3200 }
]
}
E2E OK — aggregate combines 2 devices: 240 TH/s = 100 + 140, 6200 W = 3000 + 3200That JSON is the point. It came from a route the Gateway did not have until the example mounted a plugin that declared it.
Read the two files that produced it
The plugin is a directory with a manifest and a controller, and nothing else.
gateway-plugin/mdk-plugin.json declares the route: an id, the handler path, and an http block giving the
method and path. It also carries the response schema, constraints, errors, and safety: "read-only", which is what makes the
route self-describing to both a reader and an agent.
gateway-plugin/controllers/fleet-summary.js is the handler. Every controller is
async function (req, services) and returns a plain object, which the Gateway serialises. It never touches a response object:
module.exports = async function fleetSummary (req, services) {
const { mdkClient } = services
if (!mdkClient) throw new Error('ERR_MDK_CLIENT_UNAVAILABLE')
const workersResp = await mdkClient.listWorkers()
const deviceIds = (workersResp?.workers || []).flatMap(w => w.deviceIds || [])
// fan out one telemetry pull per device, then combine
}services.mdkClient is the same protocol client used everywhere else in MDK. Aggregation lives here, in the plugin. Workers are
structurally single-device, so a Worker can only ever answer for one device; combining them into a fleet total is the plugin's job.
See how it gets mounted
One option on startGateway() does it, in run.js:
await startGateway({
kernel,
port: 3210,
root: path.join(ROOT, 'gateway'),
extraPluginDirs: [path.join(__dirname, 'gateway-plugin')]
})extraPluginDirs is the whole mounting story. Point it at a directory holding an mdk-plugin.json and its routes are served.
The manifest declares "auth": true, yet the request in this example succeeds without a token. A default 0.6.0 Gateway registers
no authentication plugin, so there is nothing to enforce the flag. Treat auth: true as a declaration of intent that an
authentication plugin would honour, not as protection you get for free.
What just happened
- Mock devices started two
SimMinerMockservers on ports15101and15102, each reporting its own hashrate and power so the totals are provably a sum rather than a single reading doubled. - Worker runtime constructed one
WorkerRuntimefrom the example's worker plugin, hosting both devices over a single RPC channel to the Kernel. One runtime means one plugin type and N devices, not one process per device. - Kernel started and discovered the runtime, reporting
sim-rack-1 [READY] devices=SIM-001,SIM-002. - Gateway started with
extraPluginDirspointing at the plugin directory, read itsmdk-plugin.json, and registeredGET /api/fleet/summary. Without that option the Gateway would have served no data routes at all. - The request hit the controller, which called
listWorkers()to enumerate device IDs, pulledmetricstelemetry for each one, and summedhashrate_rtandpowerinto fleet totals. - The assertion compared those totals against the mock configuration, printed
E2E OK, shut the Kernel down, and exited zero.
Cleanup
None. The example removes its own state directory on start and exits when the check passes.
Next steps
- Write your own plugin by building a Gateway plugin: the manifest fields, the
servicesbag, reading hardware, and sending commands - Put a page in front of your route by building a minimal single-page dashboard: the same shape with a React front end
- Integrate your own hardware by building a third-party Worker
- See the whole stack instead of one route by running a mining site end to end