MDK Logo
SupportCommunityHackathons

0.13.0 Build on MDK

Seoul Hackathon Nov 4th 2026

What is MDK?

MDK docs · MDK on GitHub

MDK, the Mining Development Kit, is an open-source framework that lets your code read and control any kind of device or service. The same building blocks serve a home miner on a shelf and a site with thousands of machines.

Every miner turns electricity into hashrate and heat. At home, that makes it a heater that earns bitcoin; at scale, it makes a site one of the most flexible loads on the grid. MDK is the data and control layer that lets software and AI agents see those machines and steer them.

You build on two pieces:

  • Workers connect a device or service to MDK: a miner's firmware, a power meter, a cooling system, or an API such as a power-price feed. A Worker is a declarative contract filled in from the device's spec, designed so a coding agent can write one.
  • Apps are standalone projects on top. They read live data and send commands through MDK, and several apps can run against the same site.

Where your code plugs in · 4 layers

Data flows up through MDK to your app, and commands flow back down. Write a Worker when MDK can't reach a device yet, and an app when it already can.

You don't have to start from zero: there's already a set of Workers built by the industry that you can use. Every Worker comes with mock data, so you can test against it without hardware.

The MDK CLI is the entry point for both. MDK is open source under Apache 2.0 and vendor-neutral, and it runs on your own infrastructure.

Mining comes first, but the goal is bigger: a standard for how intelligent systems work with the physical world, from mining sites to data centers and robotics. What you build at this hackathon is an early piece of it.

Challenges and prizes

MDK has two challenges at this hackathon, each with one winner who takes home a Heatbit Bitair, an air purifier with a built-in solo Bitcoin miner, courtesy of Heatbit.

For both, you can build an MDK app, a new Worker for a device or service MDK doesn't support yet, or both. No hardware is needed: you can build against MDK's mock environment. The ideas below are starting points, not a checklist.

Hashrate at home

A home miner is a heater that earns bitcoin. This prize goes to the best project built on MDK that makes mining useful at home: it heats what needs heating, reacts to what's happening in the house, and runs without supervision.

Ideas

  • Heat a pool, hot tub, water tank or greenhouse, mining only as hard as the heat is needed
  • A comfort controller that runs miners based on room temperature, air quality and quiet hours
  • Mine on solar surplus or off-peak power, and back off when electricity gets expensive
  • Show the real cost of heat: sats earned versus a regular electric heater

What judges look for

  • Real problem: would someone mining at home install it and keep using it?
  • It works: a live demo on real hardware or the mock environment beats slides
  • Built on MDK: telemetry and control go through MDK, not around it
  • Everyday UX: a non-technical household can set it up and understand it
  • Safe by default: respects device limits and fails safe if a sensor or the network drops
  • Bonus: open source and documented, so others can build on it

Hashrate at scale

A mining site is one of the most flexible large loads on the grid: it can shed power within seconds and bring it back on demand. This prize goes to the best project built on MDK that turns that flexibility, and the data behind it, into value for operators, energy producers or the grid. Workers can wrap APIs too, such as a power-price feed or a grid signal.

Ideas

  • Curtailment: throttle or power down automatically on price spikes, grid alerts or a site power cap, then ramp back safely
  • Demand response: answer grid signals fast enough to sell flexibility, and prove the megawatts delivered
  • Profit-aware control: set each machine's power target from hashprice, energy cost and its own efficiency
  • Follow the output of a solar or wind farm, or a flare-gas site
  • Site health: catch failing machines, hot spots or cooling problems early, or build an AI agent that proposes fixes for an operator to approve

What judges look for

  • Realism: would an operator trust it with a real fleet, and does it match how sites and energy markets work?
  • Safety: gradual ramps, hardware and contract limits respected, sensible behavior when data or connectivity is missing, a manual override
  • Measurable impact: show the numbers (cost saved, MW curtailed, response time, revenue) on real or simulated data
  • Built on MDK: telemetry and control go through MDK, not around it
  • Scale and demo: works from one rack to thousands of machines, and the demo actually runs

How to

Prerequisites

Both paths below need:

Cloning the example site, or any site run under PM2 rather than mdk run, also needs:

  • PM2 (npm install -g pm2)
  • Outbound UDP to the HyperDHT bootstrap nodes

Conference WIFI may block: Set a private DHT if it does

Run a mining site end to end and Run the CLI with npm carry the maintained lists.

There are three ways to build with MDK, and you can mix them:

  1. Run a site: put existing workers and apps together into a running site.
  2. Write a worker or an app for your own site.
  3. Publish a worker or an app for any site to install.

For both challenges, the usual entry is: start from a site that runs on mocks, then add a worker for the signal you need (a temperature, a price, a grid event). Finally, write an app that reads the data and sets miner power.

The example site, https://github.com/tetherto/mdk-app-example-site, does all of this. It runs three miner brands, four pools, a power meter and five apps against mock devices. See its README, section "Dependencies and how they are integrated".

MDK npm packages

(all 0.13.0, scope @tetherto)

PackageWhat it gives you
mdk-cliThe mdk command, which runs and checks a stack: onboard (writes mdk.yaml), create worker, create app, create dashboard, add worker and add app (from npm or GitHub), validate, run, status, skill add
mdk-skillAgent skills (SKILL.md) for coding agents such as Claude Code or Cursor. Install them with mdk skill add
mdk-kernelThe Kernel: device and worker registry, access policy, command routing
mdk-coreBootstrap helpers for the Kernel and Gateway, local discovery, and services for workers (logs, alerts, stats, settings)
mdk-workerThe worker runtime (WorkerRuntimeV2). It hosts a worker plugin's devices and connects them to the Kernel
mdk-gatewayThe HTTP gateway. It serves app routes (/apps/<name>/…) and the UI
mdk-app-runtimeRuns one app's controllers in its own process
mdk-clientClient that calls the Kernel over HRPC
mdk-identity, mdk-rbacUsers, service accounts and tokens, and role-based access checks at the Gateway, MCP server and Kernel
mdk-mcpMCP server that exposes app routes as tools for AI agents
mdk-ui-foundationHeadless UI state (Zustand stores, TanStack Query client)
mdk-react-adapterReact bindings, and the host side of app UI modules (MdkAppProvider, checkAppUiModule)
mdk-react-devkitReact UI primitives and mining components: charts, tables, header stats, sidebar
mdk-ui-agentCo-pilot chat panel for the agent app

To check which workers already exist, see the list of supported workers

The example apps are on GitHub only, not on npm. Install them from GitHub (mdk add app github:tetherto/<repo>, or a git+https://… dependency):

Start here: Hashrate at home

Starting site. A home setup is usually one miner, a meter or smart plug, and one or two sensors. You can get there in two ways:

  • mdk onboard with one miner worker on a mock. For example, mdk add worker sealminer/sealminer-mdk-worker adds one SealMiner with config.mock: true. mdk run can only mock a worker that ships mock/server.js. Among the miner workers that's SealMiner only. The WhatsMiner and Antminer mocks run only in the example site, which starts them itself.
  • Trim the example site. Keep one miner in config/devices.json, and delete the roles you don't need from pm2.roles (the other miner brands, the pools you don't use, app-demo).

What already exists:

  • Miner power control: the commands are listed under "Read data and control miners" below.
  • Electricity use: the miner's power telemetry, or a site meter (the SATEC power meter worker).
  • Earnings: the OCEAN pool worker's estimated_earnings and balance.

What you'll likely write. Write a worker for each home signal: room or water temperature, air quality, solar output, electricity tariff, quiet hours. Each is usually small. Start from one of these:

  • A sensor or plug on your LAN: copy a miner worker's structure, and keep only its telemetry and the one or two commands you need.
  • A web API, such as a tariff or a solar forecast: use ocean-mdk-worker as the model. It wraps a pool's HTTP API and reports telemetry only, with no commands. Give it a mock/ that replays a realistic day: a cold morning, a price spike, a sunny afternoon.

The app. A control loop that runs on its own:

  1. Read the room temperature (or tariff, or solar surplus).
  2. Pick a power mode or power percentage for the miner.
  3. Send it.
  4. Check the result on the next telemetry poll.

Run the loop from the app's main (see mdk-app-operation-center/plugin/main.js for a background sampler). Give it a small UI page so a household can set a target temperature and quiet hours, and see what the miner is doing and why.

The "real cost of heat" comparison. Mining heat versus an electric heater is a calculation over data MDK already has:

  • Electricity used: energy in kWh from the miner's power (or the meter) × your tariff.
  • Earnings: sats from the pool worker's estimated_earnings, or from the hashrate and network difficulty.
  • The comparison: an electric heater turns the same kWh into the same heat, with no sats. The /api/reporting/financials and /api/reporting/costs routes in the Reporting Tool show one way to structure these numbers.

Start here: Hashrate at scale

Starting site. Use the example site as it is. It has 10 miners in 3 containers, a site power meter, pools, the Operation Center and the Reporting Tool. To show scale, add more mock miners to config/devices.json. Each entry gets its own mock, so the only limit is your machine. Add them to the Operation Center's layout too, so they show up in containers.

What already exists:

  • Commands across many miners: the Operation Center sends power commands to many miners at once (POST /apps/op-center/api/commands/power-mode and /power-pct). It waits for each to succeed, so read plugin/lib/commands.js before writing your own.
  • Measured site power: SATEC meter readings at GET /apps/op-center/api/power-meters, the history at /api/history, and the Reporting Tool's /api/reporting/consumption. Use these to prove the megawatts curtailed and the response time.
  • Efficiency per miner: the efficiency and hashrate_ideal telemetry, plus the Reporting Tool's hardware catalogue (config/reporting/hardware-catalogue.json) with nominal TH/s and watts per model. These let you rank which machines to turn down first.
  • AI agent with operator approval: the agent app (@tetherto/mdk-app-agent) runs chat sessions that ask an operator to approve an action before it runs. @tetherto/mdk-ui-agent gives you the chat panel. The mcp role exposes every app's routes as tools. Routes marked "safety": "write" stay hidden unless writes are allowed, so an agent can read freely but can't act on its own.

What you'll likely write:

  • A worker for each market signal: energy price (day-ahead or real-time), a grid frequency or demand-response event, a site power cap, or the output of a solar or wind farm. Model it on ocean-mdk-worker (an API wrapped as telemetry), and give it a mock that can trigger a price spike or a grid event when you ask for one in a demo.
  • A controller app: it reads those signals and the per-miner efficiency, then sets power targets.
    • Give it a dashboard page with the current target, what the site actually draws, and an event log.
    • Add a report that adds up MW curtailed, response time, and cost saved or revenue earned.

Read data and control miners

Apps talk to the Kernel through the client the app runtime hands them (require('@tetherto/mdk-gateway/plugin').config.mdkClient). The calls the example apps use are:

CallUse
listWorkers()Find the workers and their devices
getCapabilities(deviceId)Which telemetry and commands a device supports
pullTelemetry(deviceId, …)Latest values: hashrate_rt, power, efficiency, temperature, power_mode, status, …
sendCommand(deviceId, command, params)Queue a command. It returns { commandId, status: 'QUEUED' }
getCommandStatus(commandId)Did it succeed, fail or time out

Power commands differ by brand. Check getCapabilities before sending one:

WorkerPower commands
whatsminer-mdk-workersetPowerMode { mode: low | normal | high | sleep }, setPowerPct { pct: 0–200 }
antminer-mdk-workersetPowerMode { mode: low | normal | high | sleep }, setHashratePercent { pct }
sealminer-mdk-workersetPowerMode { mode: low_power | normal | high_performance | sleep }, setPowerLimit { watts: 2000–20000 }, suspend, resume

sleep and suspend stop the miner. Use them for a full shutdown, and make sure your app can bring miners back. The app needs devices: write in its mdk-app.yaml permissions to send commands, and devices: read plus workers: list to read telemetry.

Safety (both challenges)

Judges weigh safety heavily. Build these in from the start:

  • Ramp gradually. Step setPowerPct or setPowerLimit over seconds or minutes, and stagger miners. Never switch a whole site at once.
  • Respect limits. Clamp every target to the ranges the worker's contract declares, the site or contract power cap, and the household's comfort settings. Validate before you send.
  • Fail safe. When a sensor, price feed or the network goes silent, fall back to a defined state: hold the current setting, or drop to a safe low mode. Don't keep acting on stale data. Watch telemetry timestamps and the miner status.
  • Check that it worked. Follow every command with getCommandStatus and the next telemetry value. Retry or alert on failure.
  • Manual override. Give operators a switch that pauses automation, and show when the automation is in control. The Operation Center's commandsEnabled flag is one example of turning writes off.
  • Least access. Request only the permissions your app needs. Keep write routes marked "safety": "write" so that MCP agents need explicit permission to use them.
  • Demo the failures. Kill the price mock or the sensor mock live and show what your app does.

1. Build a site from existing workers and apps**

The quickest start is the mdk CLI:

npm i -g @tetherto/mdk-cli@0.13.0
mkdir mdk-site-demo && cd mdk-site-demo
mdk onboard
# interactive: writes mdk.yaml and package.json, can add dashboard and MDK skill
mdk add worker sealminer/sealminer-mdk-worker
# a miner with a bundled mock (mock: true)
mdk add app github:tetherto/mdk-app-operation-center
mdk run
# starts the Kernel, workers, apps and Gateway (Ctrl+C stops)
# open second terminal
cd mdk-site-demo
mdk status
# add new user
cd mdk-site-demo
mdk user add {username} --admin
# to run UI
cd mdk-site-demo
mdk run dashboard
# UI link - http://localhost:3030/
# or: cd apps/dashboard && npm run dev

Notes from a test run with @tetherto/mdk-cli 0.13.0:

  • mdk onboard needs a real terminal. It fails when piped or run from a script.
  • Ports: the Gateway listens on 3847 and the Kernel on 3848. If another stack already uses those ports, mdk run stops with "port already in use". Change spec.gateway.port and spec.kernel.port in mdk.yaml.
  • The example apps aren't on npm, so mdk add app @tetherto/mdk-app-… fails with a 404. Use the github: form above.
  • Workers without a mock/server.js (WhatsMiner, Antminer) are added without a mock, and their device opts are empty. Fill them in for real hardware.
  • Sign-in: the Gateway made by mdk run requires sign-in, so app routes answer 401 to anonymous requests. See mdk user and mdk login.

You can also clone mdk-app-example-site and edit it. It wires the same packages by hand under PM2 instead of mdk run:

  • Kernel and Gateway: config/site.deploy.json sets the ports and the data directory (.site-data/). It also lists the processes to start under pm2.roles.
  • Devices: config/devices.json lists the miners and meters. Each worker has a matching mock, so no hardware is needed.
  • Apps: listed in gateway.pluginDirs.
  • Run it: npm run setup, then npm start. The site is at http://localhost:3000.

2. How vendor workers are integrated

Vendor workers are worker plugins published in the vendor's own GitHub repo.

Examples are whatsminer-mdk-worker, antminer-mdk-worker, sealminer-mdk-worker, ocean-mdk-worker, emcd-mdk-worker, spiderpool-mdk-worker and foundry-mdk-worker.

Each one is a package with these parts:

  • mdk-contract.json: what the device reports and which commands it takes;
  • handlers that talk to the device;
  • a mock/ server that pretends to be the device.

In the example site :

  1. The worker is a Git dependency in package.json, pinned to a commit: "whatsminer-mdk-worker": "git+https://github.com/whatsminer/whatsminer-mdk-worker.git#<sha>".
  2. An adapter in backend/adapters/ (for example whatsminer-adapter.js) hosts the plugin on WorkerRuntimeV2 from @tetherto/mdk-worker. It turns the devices in config/devices.json into the runtime's device list.
  3. backend/site.js starts the worker and registers it with the Kernel. It also starts the vendor's mock on a local port, with one mock device per configured device.
  4. deploy/run-process.js gives each worker its own PM2 roles, such as worker-antminer and mocks-antminer.

With mdk run, steps 2 to 4 aren't needed: mdk add worker <npm name | github:owner/repo> installs the worker and adds it to spec.workers. If the worker ships mock/server.js, mdk add sets config.mock: true and mdk run starts one mock per device. Otherwise you fill in the device opts for real hardware.

3. How workers from the MDK repo are integrated**

Some workers still live inside the MDK monorepo (https://github.com/tetherto/mdk, under backend/workers/…), for example the SATEC PM180 power meter. They aren't published to npm, and they require files elsewhere in the monorepo (backend/core, backend/workers/mock). npm can't install a single subfolder of a Git repo either.

So the example site vendors them instead:

  • npm run setup:satec (deploy/fetch-satec.js) downloads the MDK release tarball that matches the installed @tetherto/mdk-core version.
  • It extracts the worker and the paths it needs into vendor/mdk-satec/ (gitignored) and runs npm install there.
  • backend/site.js then loads the worker's startSatecWorker and its mock from that folder and starts them, like the other workers (PM2 roles worker-satec and mocks-satec).

You can follow the same pattern for any other worker in the MDK repo.

4. How apps are integrated

An app is a package with:

  • mdk-app.yaml: its id, version, the Kernel permissions it needs, an optional background main, and default config;
  • plugin/mdk-plugin.json: its HTTP route table, with the controllers that handle each route;
  • optionally, a UI module in ui/ built with defineMdkApp (routes, nav sections, panels).

In the example site:

  1. Install it. The app is a dependency in package.json, from npm, Git or a file: path. For example: "@tetherto/mdk-app-operation-center": "git+https://github.com/tetherto/mdk-app-operation-center#<ref>".
  2. Configure it. Add a gateway.pluginDirs entry: { "name": "op-center", "package": "@tetherto/mdk-app-operation-center", "config": { … } }. Then add app-op-center to pm2.roles.
  3. Run it. Each app runs in its own @tetherto/mdk-app-runtime process. It signs in to the Kernel as app:<name>, which @tetherto/mdk-rbac grants exactly the permissions listed in its mdk-app.yaml. Its routes are served at /apps/<name>/….
  4. Show its UI. ui/src/installedApps.tsx imports the app's ui/index.js and ui/style.css, then validates the module with checkAppUiModule. It mounts the app's pages at #/app/<name>/… inside MdkAppProvider, which sends the app's API calls to /apps/<name>. The app's routes also become a group in the left nav. ui/vite.config.ts dedupes React and the @tetherto UI packages, so the apps share the host's copies.

With mdk run, steps 2 and 3 are done by mdk add app <npm package | github:owner/repo>. It adds the app under spec.apps and writes its service account and its role in spec.rbac. mdk create dashboard scaffolds a UI shell that loads app UIs.

5. Add your own worker or app to a site

A worker is for a device or API that MDK can't reach yet: a heater, a smart plug, a thermostat, a power-price feed, a grid signal.

  1. Scaffold it with mdk create worker <name> in the project. It writes workers/<name>/ with mdk-contract.json, src/, mock/server.js and a smoke test, and registers the worker in mdk.yaml with mock: true. Without an mdk.yaml it still scaffolds the worker and prints the entry to add. Or ask a coding agent with the MDK skill installed (mdk skill add --client claude|cursor|all).
  2. Fill in mdk-contract.json from the device's spec, write the handlers, and add a mock/server.js. The mock is what you'll demo with, so make it easy to trigger the interesting cases.
  3. Add it to the site:
    • mdk run sites: list it by local path under spec.workers (package: ./workers/<name>).
    • The example site: add it as a file: dependency, then add an adapter (copy backend/adapters/sealminer-adapter.js), a boot…Worker function in backend/site.js, the mock and worker roles in deploy/run-process.js, and its devices in config/devices.json.

An app reads telemetry and sends commands: a curtailment controller, a heat scheduler, an efficiency report.

  1. Scaffold it with mdk create app <name>. It writes apps/<name>/ with mdk-app.yaml, plugin/ (route table and controllers), and a UI in ui-src/, built to ui/ with npm run build:ui. It also registers the app and its access role in mdk.yaml.
  2. Declare its routes in plugin/mdk-plugin.json, marking each one "safety": "read-only" or "write". Declare its Kernel permissions in mdk-app.yaml, requesting only the access it needs.
  3. Put any control loop in the app's main.
  4. Optionally, add a ui/ module made with defineMdkApp and @tetherto/mdk-react-devkit.
  5. Add it to the site:
    • mdk run sites: add it under spec.apps (package: ./apps/<name>).
    • The example site: add a gateway.pluginDirs entry with "dir": "../my-app" (or a file: dependency and "package"), add app-<name> to pm2.roles, and add its UI to INSTALLED_APPS in ui/src/installedApps.tsx if it has one.
  6. Check it with mdk validate app <name>.

6. Publish a worker or app for others

A worker or app doesn't have to live inside a site. Build it as its own repo, so any MDK site can install it. This also earns the judges' "open source and documented" bonus:

  • Package name: name it like the existing ones (<vendor>-mdk-worker, mdk-app-<name>).
  • Package contents: a worker ships mdk-contract.json, its handlers and mock/. An app ships mdk-app.yaml, plugin/ and the built ui/. Use the files field in package.json to choose what gets published.
  • Dependencies: list @tetherto/mdk-* as peerDependencies (or with a compatible range), so the site's copies are used.
  • Development setup: keep a small mdk.yaml dev stack in the repo (for example dev/) that runs your package against mocks with mdk run. Run mdk validate app|worker --path . --smoke --strict in CI.
  • Publishing: publish to npm, or tag a GitHub release. Sites then install it with mdk add worker|app <package> or github:owner/repo#tag, or as a Git dependency as the example site does.
  • Docs: document the config keys, the permissions it asks for and its safety behavior in the README. Add a mock so others can try it without hardware.
  • Listing: open a PR to add a worker to the supported-workers list.

Before you demo

  • It runs on mocks from a clean clone with one command, and the README says which one.
  • It shows numbers: for home, comfort held, and sats versus heater cost; for scale, MW curtailed, response time, and cost saved or revenue earned.
  • It shows a failure: a sensor, feed or the network drops, and the system ends up in a safe state.
  • It goes through MDK: telemetry comes from pullTelemetry and control from sendCommand. It never talks to a miner's own API directly.

On this page