# Supplying compute to rent.dev

You can sell sandbox time on your own machines through rent.dev. This guide is for you and for the coding agent doing the setup. It is built from the same tables the platform runs on, so the codes, states and commands here are the ones you will see.

Most of the work is one command. After that, every answer the platform gives carries a `next` step, so an agent can finish the job by following it.

## What a node is

A node is one machine, or one backend of yours, that runs our node kit. It belongs to an ordinary rent.dev account. You can have many.

A node holds its own key. It opens an outbound tunnel of its own, so it needs no open inbound port. Every request it sends is signed with its key, and every request the platform sends it is signed with a platform key that the node checks. A node's key works for that one node and nothing else.

When `rentdev-node up` makes the account for you, it also keeps that account's agent key on the machine, in `/etc/rentdev-node/account.json`, readable by root only. That key is the only way back into the account and its earnings, so keep a copy of the file somewhere safe. A machine set up with a claim token made elsewhere holds no account key at all.

Before a node takes paid work, the platform tests it: a conformance probe, then a real lease the platform pays for. You do not review anything by hand, and neither do we.

## Pick a path

- **A host.** A Linux x86_64 machine with KVM and Docker. The installer runs inside our image and sets up the sandbox stack for you: open-source software that runs each sandbox as a microVM. The host needs Docker and nothing else, not even Node. Pick this when you have hardware.

- **A driver file.** You already run sandboxes on a backend of your own. You write one small module that creates, lists, runs commands in and kills sandboxes, and the kit serves it. Your machine needs Node 24, and Docker for the node's tunnel. Pick this when you have a backend but not a spare host.

## Path 1: a host

A fresh host has no `rentdev-node` command, so until `up` has run, each command goes through the image:

```sh
sudo docker run --rm -i --privileged --network host --pid host -v /:/host -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/ezolabs/rentdev-node@sha256:0daf745082abbb219ea0bdad0991db10fc84c3b944d876c325272b57eacd04eb <command>
```

`up` writes `/usr/local/bin/rentdev-node`, a short script around that same line. From then on `rentdev-node <command>` runs on the host as written, which is how this guide and every `next.command` print it.

1. Check the host. This changes nothing:

```sh
sudo docker run --rm -i --privileged --network host --pid host -v /:/host -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/ezolabs/rentdev-node@sha256:0daf745082abbb219ea0bdad0991db10fc84c3b944d876c325272b57eacd04eb check
```

2. Bring the node up:

```sh
sudo docker run --rm -i --privileged --network host --pid host -v /:/host -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/ezolabs/rentdev-node@sha256:0daf745082abbb219ea0bdad0991db10fc84c3b944d876c325272b57eacd04eb up --region eu-central --cpu-ask 0.02 --memory-ask 0.0025 --slots 8
```

3. If it stops early, run the `next.command` it printed, or `rentdev-node status --wait`, until the action is `none`.

`up` works in this order, and names each change as it makes it:

- the preflight checks;

- the sandbox stack, which runs the microVM sandboxes, and its memory pool, then the templates;

- a firewall table that closes the sandbox stack's ports to the network, and `rentdev-node-guard.service`, which loads it at boot;

- the preflight checks once more;

- the claim: with no account it makes one here, then enrols the node and claims it;

- the node itself, and `/usr/local/bin/rentdev-node`.

Then it follows the node until it can take leases, and prints how long each phase took. Run again, it resumes the node and changes nothing that is already right.

If you made a claim token elsewhere (see [Enrol through the API](#enrol-through-the-api)), run the install line it came with instead:

```sh
sudo docker run --rm -i --privileged --network host --pid host -v /:/host -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/ezolabs/rentdev-node@sha256:0daf745082abbb219ea0bdad0991db10fc84c3b944d876c325272b57eacd04eb up --claim-stdin <<< '<claim_token>'
```

The token arrives on stdin through the `<<<` here-string, never as an argument, so it never shows in a process list. The here-string needs bash or zsh.

A claim token lasts 10 minutes, and on a host the claim comes after the install. A first install on a fresh host can take longer than that, so the token can run out on the way. Nothing is lost. When the line ends with `claim_invalid` at the claim step, make a new token and run the same line again: that run is quick, because the host is already set up.

What each flag gives the installer:

| Flag | What it gives |
| --- | --- |
| `--rm` | Removes the installer container when it exits. |
| `-i` | Keeps stdin open, so the claim token arrives on stdin. |
| `--privileged` | Lets the installer act on the host: load the firewall table, set hugepages and start services. |
| `--network host` | Runs on the host's network, so preflight sees the host's real ports and routes. |
| `--pid host` | Shares the host's process table, so the installer can enter the host's namespaces (`nsenter -t 1`). |
| `-v /:/host` | Mounts the host's filesystem at /host, to read host facts and write the node's files under /etc/rentdev-node. |
| `-v /var/run/docker.sock:/var/run/docker.sock` | Uses the host's Docker to start the sandbox stack and the node's own containers. |

Every change `up` makes on the host is named in its output, and `rentdev-node down` undoes it.

## Path 2: a driver file

1. Write the driver (the contract is below). The package ships a complete reference driver on the local Docker CLI, `createDockerDriver` in `rentdev-node/drivers/docker`, short enough to read in one sitting.

2. Test it with no account and no network. This runs the platform's probe checks against your driver and prints one line per check, with its fix:

```sh
npx rentdev-node@0.1.0 up --mock --driver ./driver.mjs
```

3. Bring it up for real:

```sh
npx rentdev-node@0.1.0 up --region eu-central --cpu-ask 0.02 --memory-ask 0.0025 --slots 8 --driver ./driver.mjs
```

4. Follow it. A driver machine has no `rentdev-node` on its PATH, so every command, and every `next.command`, runs through `npx`:

```sh
npx rentdev-node@0.1.0 status --wait
```

With a claim token made elsewhere, the line is:

```sh
npx rentdev-node@0.1.0 up --driver <driver_file> --claim-stdin <<< '<claim_token>'
```

Put your driver's path in place of `<driver_file>`.

On your machine a driver node is two things: its tunnel, which runs in a container, and the node itself, a `rentdev-node serve` process that `up` starts in the background. That process does not come back after a reboot. Run `up` again then, or keep `rentdev-node serve --driver <driver_file>` under your own process manager.

The node's files go in `/etc/rentdev-node`, which takes root. Set `RENTDEV_NODE_DIR` to a directory you own to keep them elsewhere.

### The driver contract

```js
import { defineDriver } from "rentdev-node";

export default defineDriver({
  name: "my-backend",
  version: "1.0.0",
  async capacity() { /* { slots, templates } */ },
  async create({ leaseId, template, cpuCount, memoryMb, timeoutS, env, metadata }) { /* a sandbox */ },
  async get(id) { /* the sandbox, or null */ },
  async list({ metadata }) { /* the sandboxes whose metadata matches */ },
  async kill(id) { /* true, or false when already gone */ },
  async exec(id, { command, cwd, env, timeoutS }) { /* { stdout, stderr, exitCode, timedOut } */ },
});
```

- Required: `capacity`, `create`, `get`, `list`, `kill` and `exec`.

- A sandbox is `{ id, template, state, cpuCount, memoryMb, startedAt, endAt, metadata }`. Its `id` is one URL path segment: letters, digits, `_`, `.` and `-`, at most 128 characters.

- `capacity` answers how many sandboxes run at once and the template aliases you serve. The probe needs the alias `base`.

- `create` must be idempotent on `leaseId`: a second call with the same id returns the same sandbox.

- `kill` returns `false` when the sandbox is already gone. The probe kills twice and checks this.

- `list({ metadata })` must find sandboxes by their lease metadata: every pair given has to match. The probe checks that none are left.

- `exec` runs `command` under `sh -c` inside the sandbox. `exitCode` is `null` when the command was killed.

- Optional: `pause` and `resume` (as a pair or not at all), `setTimeout`, `readFile`, `writeFile` and `listFiles`. File calls are built from `exec` when you leave them out.

- For an outcome the caller should see, throw the package's `DriverError` with one of the codes `not_found`, `conflict`, `invalid_request`, `capacity` or `unsupported`. Anything else you throw counts as a fault of the driver.

- A verb your driver lacks is simply not offered to customers.

- `runDriverConformance`, from `rentdev-node` too, runs the probe's sandbox checks straight against a driver, for your own test suite.

<a id="enrol-through-the-api"></a>

## Enrol through the API

Use this for fleets, or when your agent already has an account. Enrolment routes need an access token with the `supply` scope. Get one with the `client_credentials` grant and a `private_key_jwt` assertion signed by your account's agent key, asking for `scope=supply`. API keys never carry this scope. The authorization server is listed at `https://api.rent.dev/.well-known/oauth-protected-resource`.

`POST https://api.rent.dev/v1/supply/enrollments`:

```json
{
  "wire": "e2b",
  "region": "eu-central",
  "ask": {
    "cpu_usd_per_vcpu_hour": "0.02",
    "memory_usd_per_gib_hour": "0.0025"
  },
  "capacity": {
    "slots": 8
  },
  "count": 1
}
```

`count` makes up to 1000 claim tokens at once, one per node. `wire` is `e2b` for a host and `rent` for a driver. `payout` is optional. The answer:

```json
{
  "id": "enr_…",
  "claim_tokens": [
    "rent_ec_…"
  ],
  "expires_at": "2026-01-01T00:10:00Z",
  "install": {
    "host": "sudo docker run --rm -i --privileged --network host --pid host -v /:/host -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/ezolabs/rentdev-node@sha256:0daf745082abbb219ea0bdad0991db10fc84c3b944d876c325272b57eacd04eb up --claim-stdin <<< '<claim_token>'",
    "driver": "npx rentdev-node@0.1.0 up --driver <driver_file> --claim-stdin <<< '<claim_token>'"
  },
  "guide_url": "https://api.rent.dev/v1/supply/guide"
}
```

Claim tokens are shown once. Each works once, within 10 minutes. Run the install line on each machine with its own token. At most 10 of an account's nodes may be linking at once: a claim past that answers `linking_limit` and leaves its token unused, so run the same line again after `retry_after` seconds. When the token would expire before a node leaves linking, `next.action` is `re_enroll` instead: make a new token after `retry_after` seconds and run the line with that. Over MCP, `rent_supply_enroll` makes the same enrolment and also answers `lines`: the exact line for each token. The CLI then claims the token with `POST https://api.rent.dev/v1/supply/claims`, which needs no other credential:

```json
{
  "claim_token": "rent_ec_…",
  "public_key": "<node public key, base64url>",
  "signature": "<node key over the claim message, base64url>",
  "agent_version": "0.1.0",
  "wire": "e2b",
  "facts": {
    "fingerprint": "<sha256 hex of the host's identity>"
  },
  "checks": []
}
```

`signature` is the new node key's signature over `rent.dev claim v1\n{claim_token}\n{public_key}`. `checks` holds the preflight results. If a blocking check fails, the claim answers `checks_failed` and the token stays unused until it expires, so you can fix and retry.

## The loop: states and next steps

Every node view and every beat answer carries `next`: `{ action, command, after_s }`. The whole loop is: wait `after_s` seconds, run `next.command`, and repeat until the action is `none`.

| State | What it means | Next action | Command |
| --- | --- | --- | --- |
| `linking` | Claimed. The node is bringing up its tunnel, and the platform checks that it can reach the node. | `wait` | `rentdev-node status --wait` |
| `probing` | The platform runs its conformance probe, then buys a first audit lease. | `wait` | `rentdev-node status --wait` |
| `probation` | Passed. Requests that ask for all partner supply can be placed here. Audits go on. | `none` | none |
| `active` | Promoted. Customer requests can be placed here by default; one that sets `none` is not. | `none` | none |
| `suspended` | Takes no leases. It may still send beats and events, relink and ask for a probe. Fix the cause, then run `rentdev-node doctor`: it mends the link if it must, and asks for a probe. | `fix_checks` | `rentdev-node doctor` |
| `revoked` | Gone for good, with its link removed. Enrol the machine again to come back. | `re_enroll` | `rentdev-node up --re-enroll` |

The platform can override a state's action: `upgrade` when the node agent is too old, `fix_checks` when a blocking check fails, and `fix_checks` when it does not reach a node past linking. That last `rentdev-node doctor` repairs the link when you run it; the long-running node agent only logs that the node is not reached.

| Action | Command | When |
| --- | --- | --- |
| `start_link` | `rentdev-node up` | The claim worked. Start the node. |
| `wait` | `rentdev-node status --wait` | The platform is working. Wait `after_s` seconds and follow the node. |
| `fix_checks` | `rentdev-node doctor` | A check failed. Read each fix, apply it, and run the checks again. |
| `upgrade` | `rentdev-node upgrade` | The node agent is too old. |
| `re_enroll` | `rentdev-node up --re-enroll` | The token or the node is no longer usable. Make a fresh enrolment. |
| `none` | none | Nothing to do. |

Run each command as it is printed. Three things to know:

- On a driver machine the commands run through `npx`: `npx rentdev-node@0.1.0 status --wait` for `rentdev-node status --wait`.

- `rentdev-node up --re-enroll` makes a new enrolment under this machine's account, with the `--region`, `--cpu-ask`, `--memory-ask` and `--slots` that `up` last enrolled it with. An option you give wins. `down` removes that record, so after `down` give all four. It replaces this machine's node only once that node is revoked.

- A machine set up with a claim token made elsewhere holds no account key, so there `rentdev-node up --re-enroll` stops and makes no account. Make a new token where you made the first, and run the same install line again with it on stdin and `--re-enroll` added.

How a node moves:

- `linking` to `probing` once the platform reaches the node through its tunnel and the node proves its key.

- `probing` to `probation` once the probe passes and the first audit lease is paid.

- `probation` to `active` after 7 passed audits in a row, no liveness mismatch, and no failed customer create in the last 24 hours. Customer leases are not required.

- To `suspended` after 2 failed checks in a row, a liveness mismatch, or a beat too old when a check is due. Fix the cause, then run `rentdev-node doctor`. Once its checks pass, it asks for a probe itself. Without the CLI, call `POST /v1/supply/nodes/{id}:probe`, or `rent_supply_probe_node` over MCP.

- To `revoked` when the node is still linking 15 minutes after its claim, after 7 silent days, or when you revoke it. Run `rentdev-node up --re-enroll` to come back.

- A new driver version, a changed template list, more slots or a new region sends the node back to `probing`. A new ask does not.

The node times each setup phase (`preflight`, `embed`, `templates`, `claim`, `link`, `probe`, `audit`) and the node view shows them as `phase_ms`, with `time_to_first_payable_ms` and `time_to_first_customer_lease_ms`.

## Checks

Each check result is `{ code, status, source, detail, fix, doc_url }`. The status is one of `pass`, `fail`, `warn`, `skip`. Only a `fail` on a blocking check stops progress. Preflight runs on the node; link and probe checks are run by the platform.

| Code | Phase | Severity | Fix |
| --- | --- | --- | --- |
| <a id="check-os-linux"></a>`os_linux` | preflight | blocking | Run the node on Linux (x86_64). |
| <a id="check-kvm-available"></a>`kvm_available` | preflight | blocking | Enable hardware virtualisation in the BIOS and load the kvm module so /dev/kvm exists. |
| <a id="check-cgroup-v2"></a>`cgroup_v2` | preflight | blocking | Boot with the unified cgroup v2 hierarchy (systemd.unified_cgroup_hierarchy=1). |
| <a id="check-docker-version"></a>`docker_version` | preflight | blocking | Install Docker Engine 28.3.3 or newer: older releases let LAN neighbours reach ports published on 127.0.0.1 (moby#45610, CVE-2025-54388). |
| <a id="check-docker-compose"></a>`docker_compose` | preflight | blocking | Install the Docker Compose v2 plugin. |
| <a id="check-outbound-443"></a>`outbound_443` | preflight | blocking | Allow outbound TCP 443 to the rent.dev API from this host. |
| <a id="check-outbound-7844"></a>`outbound_7844` | preflight | blocking | Allow outbound TCP or UDP 7844 to the tunnel edge network: the tunnel needs it and 443 alone is not enough. |
| <a id="check-disk-free-gib"></a>`disk_free_gib` | preflight | blocking | Free disk space on the Docker data volume. |
| <a id="check-memory-gib"></a>`memory_gib` | preflight | blocking | Add memory, or stop other workloads: at least 24 GiB must stay available once sandbox hugepages are reserved. |
| <a id="check-embed-ports-closed"></a>`embed_ports_closed` | preflight | blocking | Firewall the sandbox stack's host ports (3000-3003, 5007-5010, 5016-5018, 5109) and store ports (5432, 6379, 8123, 9000, 30006) from every non-loopback address. |
| <a id="check-clock-skew"></a>`clock_skew` | preflight | blocking | Enable NTP time sync (timedatectl set-ntp true): the node's gate refuses platform requests outside ±120 s, so this check fails above 60 s of skew. |
| <a id="check-bare-metal"></a>`bare_metal` | preflight | advisory | Prefer bare metal: nested KVM in a VM also passes /dev/kvm but runs sandboxes slower. |
| <a id="check-no-unattended-upgrades"></a>`no_unattended_upgrades` | preflight | advisory | Disable automatic reboots from unattended upgrades: a reboot ends every lease on the node. |
| <a id="check-other-schedulers"></a>`other_schedulers` | preflight | advisory | Kubernetes, Slurm or Nomad agents were found running; make sure they cannot place work on the cores and memory you offer. |
| <a id="check-link-up"></a>`link_up` | link | blocking | Start the node (rentdev-node up) and check that the tunnel container is running. |
| <a id="check-gate-answers"></a>`gate_answers` | link | blocking | Check that the gate listens on 127.0.0.1:3120 and 127.0.0.1:3122 (rentdev-node doctor). |
| <a id="check-key-proof"></a>`key_proof` | link | blocking | The ping answer was not signed by this node's key: restore /etc/rentdev-node/node.json or re-enrol. |
| <a id="check-template-base"></a>`template_base` | probe | blocking | Build the standard templates so the alias `base` resolves (rentdev-node doctor). |
| <a id="check-create"></a>`create` | probe | blocking | The node could not start a 1 vCPU, 512 MiB sandbox; read the driver logs. |
| <a id="check-exec"></a>`exec` | probe | blocking | A command inside the sandbox did not return the expected sha256 digest; check the sandbox runtime. |
| <a id="check-shape-vcpu"></a>`shape_vcpu` | probe | blocking | The sandbox saw fewer vCPUs than requested; do not cap sandbox CPUs below the request. |
| <a id="check-shape-memory"></a>`shape_memory` | probe | blocking | The sandbox saw under 90% of the requested memory; do not cap sandbox memory below the request. |
| <a id="check-cpu-bench"></a>`cpu_bench` | probe | blocking | Per-vCPU throughput fell below a third of the control's; do not oversubscribe the host's cores. |
| <a id="check-file-roundtrip"></a>`file_roundtrip` | probe | blocking | A file written in the sandbox did not read back the same; check the sandbox filesystem. |
| <a id="check-cold-start-ms"></a>`cold_start_ms` | probe | advisory | Time to a ready sandbox, measured by the platform; keep templates cached to lower it. |
| <a id="check-kill"></a>`kill` | probe | blocking | The sandbox did not stop when killed; read the driver logs. |
| <a id="check-kill-idempotent"></a>`kill_idempotent` | probe | blocking | A second kill must report the sandbox already gone, not fail or succeed again. |
| <a id="check-no-orphans"></a>`no_orphans` | probe | blocking | Sandboxes were left running after the probe; listing by lease metadata must return none. |
| <a id="check-data-plane"></a>`data_plane` | probe | blocking | The sandbox was unreachable through its public hostname; check the proxy port 3122 and the tunnel. |
| <a id="check-paid-path"></a>`paid_path` | probe | blocking | A paid lease did not start, run and settle on this node; read the driver logs for its sandbox. |

## Problems

Errors are RFC 9457 problems (`application/problem+json`) with a stable `code`. Beside the usual fields they carry:

- `retryable`: the same request may succeed if sent again later.

- `retry_after`: seconds to wait, when known. It is also the `Retry-After` header.

- `owner_action_required`: only rent.dev can clear it. Retrying will not help until we do.

- `next`: the step to run, as in the loop above.

- `checks`: when a check failed, every result with its fix.

| Code | Status | Retry | Next action | What to do |
| --- | --- | --- | --- | --- |
| <a id="problem-claim-invalid"></a>`claim_invalid` | 410 | no | `re_enroll` | A claim token works once, within 10 minutes. When `up` made the enrolment, run `rentdev-node up --re-enroll`; when the token was made elsewhere, make a new one there and run the same line again with it. |
| <a id="problem-claim-proof-invalid"></a>`claim_proof_invalid` | 401 | no | `re_enroll` | The node key's signature over the claim token did not verify. Start again with a fresh enrolment. |
| <a id="problem-checks-failed"></a>`checks_failed` | 422 | no | `fix_checks` | `checks` lists every result with its fix. Fix each blocking `fail`, then run the same command again. The claim token stays unused until it expires. |
| <a id="problem-node-exists"></a>`node_exists` | 409 | no | `none` | A live node already has this host's fingerprint, but this host's node file no longer names it. Revoke the old node (`POST /v1/supply/nodes/{id}:revoke`), then run the same command again. |
| <a id="problem-supply-link-unavailable"></a>`supply_link_unavailable` | 503 | yes | `wait` | The node's tunnel could not be set up; `detail` names the limit when one was hit. Retry after `retry_after` seconds. When `owner_action_required` is true, rent.dev has to act first. |
| <a id="problem-linking-limit"></a>`linking_limit` | 429 | yes | `wait` | At most 10 of an account's nodes may be linking at once, and this claim would be one more, so its token stays unused. When `next.action` is `wait`, retry after `retry_after` seconds, by when a node has linked or been removed; when it is `re_enroll`, the token expires first, so make a new one after `retry_after` seconds and claim with that. |
| <a id="problem-not-found"></a>`not_found` | 404 | no | `none` | No node has this id on your account. A node of another account answers the same. |
| <a id="problem-node-revoked"></a>`node_revoked` | 410 | no | `re_enroll` | The node is revoked for good. Enrol the machine again with `rentdev-node up --re-enroll`. A machine set up with a token made elsewhere needs a new token from there, on stdin, with `--re-enroll` added to the same line. |
| <a id="problem-node-suspended"></a>`node_suspended` | 403 | no | `fix_checks` | A suspended node may only send beats and events, relink and ask for a probe. Run `rentdev-node doctor` and apply each fix it reports. Once the checks pass, it mends the link if the platform cannot reach the node, and asks for the probe itself. |
| <a id="problem-unauthenticated"></a>`unauthenticated` | 401 | no | `fix_checks` | The node's signature is missing, stale or wrong. Check the clock (`clock_skew`) and that the node file is intact. |
| <a id="problem-node-replay"></a>`node_replay` | 409 | yes | `wait` | This sequence number or nonce was already used. Sign the request again with a new one. |
| <a id="problem-probe-in-progress"></a>`probe_in_progress` | 409 | yes | `wait` | A probe of this node is already running. Wait for it to finish; `rentdev-node status --wait` follows it. |
| <a id="problem-payout-proof-required"></a>`payout_proof_required` | 403 | no | `none` | Changing payout needs `signed_nonce`: a fresh nonce from `GET /v1/accounts/nonce`, and the account's agent key's signature over `rent.dev payout v2\n{node_id}\n{nonce}\n{public_key}\n{network}\n{address}`, with the address in lowercase and the network as the node will store them. |
| <a id="problem-agent-upgrade-required"></a>`agent_upgrade_required` | 400 | no | `upgrade` | The node agent is older than the platform accepts. Run `rentdev-node upgrade`. |

`not_found` and `unauthenticated` are general rent.dev codes; the table says what they mean on supply routes. Routes also answer other general codes, such as `forbidden` (a token without the `supply` scope) and `invalid_request`. Each problem's `docs_url` links to its entry in the error reference.

On a machine set up with a token made elsewhere, `rentdev-node up --re-enroll` stops with `re_enroll_needs_account` and makes no account. Make a new token where you made the first, and run the same install line again with it and `--re-enroll` added.

## The CLI

| Command | What it does |
| --- | --- |
| `rentdev-node check` | Runs the preflight checks. Changes nothing. |
| `rentdev-node up --region <label> --cpu-ask <usd> --memory-ask <usd> --slots <n>` | Brings a node up, or resumes the one this machine holds, and follows it until it can take leases. Needs no account beforehand. |
| `rentdev-node status [--wait]` | Shows the node. `--wait` follows it until `next` changes, or until the platform reaches it again. |
| `rentdev-node doctor` | Runs the preflight checks again, then shows the node's last checks and reasons, each with its fix. It repairs a broken link, also when the platform no longer reaches the node. It asks for a probe when a suspended node's checks pass and the platform reaches it. |
| `rentdev-node upgrade` | Moves the node to the version the platform asks for. |
| `rentdev-node payout [--address <0x…>] [--agent-key <file>]` | Shows the node's payout address and network, the account's releasable, in-payout and paid totals, and the node's latest payouts. `--address` registers a new address: it signs the proof with the account's agent key and changes the node. |
| `rentdev-node down [--revoke]` | Stops the node and undoes what `up` changed. `--revoke` also revokes the node. |
| `rentdev-node serve --driver <embed \| path>` | The long-running node process. `up` starts it for you. |
| `rentdev-node agent-setup` | Copies the supply skill into this project and points AGENTS.md at it. |

- `doctor` stores its results, and the node reports them on its next beat. Run it after a fix, before `status --wait`.

- When the platform's link checks fail (`link_up`, `gate_answers`, `key_proof`), `doctor` mends the link first. It takes a fresh tunnel token, rewrites the tunnel's env file and restarts the tunnel. It names each change.

- The node view's `reachable` says whether the platform reaches the node through its tunnel. When it is `false` after linking, `doctor` mends the link, suspended or not, and answers `wait`. `status --wait` then follows the node until the platform reaches it.

- `doctor` mends the link at most once in 10 minutes. Inside that time it says when it last did, and what to check on the machine: outbound TCP or UDP 7844 to the tunnel network, and the tunnel container's log. Then it answers `wait`.

- On a suspended node whose checks pass, `doctor` asks for a probe. It does so only once the platform reaches the node. It uses the account's key when the machine holds it, and the node's own key when not. Then it answers `wait`.

- On a machine with no node, `doctor` answers `start_link`. A first `up` needs `--region`, `--cpu-ask`, `--memory-ask` and `--slots`, or a claim token on stdin.

- `upgrade` on a host pulls the new image and restarts the node on it. A driver node runs from npm and cannot replace itself, so there `upgrade` prints the `npx` line to run.

Options of `up`:

| Option | What it does |
| --- | --- |
| `--region <label>` | A generic region label, such as `eu-central`. |
| `--cpu-ask <usd>` | Your ask per vCPU-hour, in USD. |
| `--memory-ask <usd>` | Your ask per GiB-hour of memory, in USD. |
| `--slots <n>` | How many sandboxes the node runs at once. |
| `--payout-address <0x…>` | The USDC address the new node's payouts go to, put in its enrolment. Change it later with `rentdev-node payout --address`. |
| `--agent-key <file>` | Uses an existing account's agent key. |
| `--claim-stdin` | Reads a claim token made elsewhere from stdin and skips to the claim. |
| `--claim-file <path>` | The same, from a file. |
| `--driver <embed \| file>` | The backend. `embed` is the default. |
| `--re-enroll` | Makes a fresh enrolment after a token expired or the node was revoked. Options you leave out are the ones `up` last enrolled this machine with. With a claim token, it claims that token instead. |
| `--pool-gib <n>` | Overrides the sandbox memory pool. The default is 80% of what is left above 24 GiB. |
| `--mock` | Runs the whole flow against a local mock of the platform: no account, no network, nothing installed. |

On a host the installer sees the host's files only, so give `--claim-file` and `--agent-key` an absolute path.

Options every command takes:

| Option | What it does |
| --- | --- |
| `--json` | Prints one JSON object per line on stdout, for a program to read. |
| `--color <auto \| always \| never>` | When to color the output. The default is `auto`: on a terminal only. |
| `--no-color` | No color. `NO_COLOR`, `RENTDEV_NODE_NO_COLOR` and `TERM=dumb` do the same. |
| `-h, --help` | Prints help for the CLI, or for the command it follows. |
| `-V, --version` | Prints the version. |

With `--json`, each line is `{ step, code, status, source, detail, fix, doc_url, next_action, next_command }`. Exit codes: 0 pass, 1 fail, 2 retry later. Every `next_command` is one of the commands in the loop table, word for word.

Environment variables:

| Variable | What it sets |
| --- | --- |
| `RENTDEV_API_URL` | The API origin. The default is production's. |
| `RENTDEV_AUTH_URL` | The sign-in origin. The default is the one the API names. |
| `RENTDEV_NODE_DIR` | Where a driver node keeps its files. The default is `/etc/rentdev-node`, which a host node always uses. |
| `RENTDEV_NODE_IMAGE` | The node image to install, by digest. The default is the image the command runs from. |
| `RENTDEV_HOST_ROOT` | Where the host's `/` is mounted for the command. The default is `/host` inside the installer's container and `/` anywhere else. |
| `RENTDEV_EMBED_DIR` | Where the sandbox stack's compose project is kept. The default is `/opt/rentdev-node/embed`. |
| `RENTDEV_NODE_NO_COLOR` | Turns color off when set, as `NO_COLOR` does. |

## Managing nodes

| Method | Path | Auth | MCP tool | What it does |
| --- | --- | --- | --- | --- |
| POST | `/v1/supply/enrollments` | `supply` scope | `rent_supply_enroll` | Makes claim tokens, one per node, and their install lines. |
| POST | `/v1/supply/claims` | none | none | Claims a token with a new node key. The CLI calls it for you. |
| GET | `/v1/supply/nodes` | `supply` scope | `rent_supply_nodes` | Your nodes. |
| GET | `/v1/supply/nodes/{id}` | `supply` scope | `rent_supply_node` | One node: state, checks, reasons, whether the platform reaches it, `next`, phase timings and placement. |
| PATCH | `/v1/supply/nodes/{id}` | `supply` scope | `rent_supply_update_node` | Changes the ask, capacity, region or payout details. |
| POST | `/v1/supply/nodes/{id}:probe` | `supply` scope | `rent_supply_probe_node` | Asks for a probe now, e.g. to leave suspension. |
| POST | `/v1/supply/nodes/{id}:revoke` | `supply` scope | `rent_supply_revoke_node` | Revokes the node and removes its link. |
| GET | `/v1/supply/earnings` | `supply` scope | `rent_supply_earnings` | Accrued earnings and recent entries. |
| GET | `/v1/supply/payouts` | `supply` scope | `rent_supply_payouts` | Payouts to your nodes, newest first, each with the call that checks it on the chain. |
| GET | `/v1/supply/keys` | none | none | The keys a node's gate verifies platform requests with. |
| GET | `/v1/supply/guide` | none | `rent_supply_guide` | This guide, as Markdown. |

The tools are on rent.dev's MCP server, `https://mcp.rent.dev/mcp`. Each answers what its route answers, and refuses with the same problem, `next` included. `rent_supply_guide` returns this guide and needs no credential, so the public endpoint, `https://mcp.rent.dev/mcp/public`, serves it too. The others need an OAuth access token with the `supply` scope; an API key gets `forbidden` from them.

`PATCH https://api.rent.dev/v1/supply/nodes/{id}`:

```json
{
  "ask": {
    "cpu_usd_per_vcpu_hour": "0.018",
    "memory_usd_per_gib_hour": "0.0025"
  }
}
```

- A new `ask` applies at once to new leases. Running leases keep the rates they started with.

- More `capacity.slots`, or a new `region`, sends the node back to `probing`.

- Changing `payout` needs `signed_nonce`: a nonce from `GET /v1/accounts/nonce`, and your account's agent key's signature, base64url, over `rent.dev payout v2\n{node_id}\n{nonce}\n{public_key}\n{network}\n{address}`: the node's id, the nonce, that key, the network and the address in lowercase, as the node will store them. Leave `payout.network` out and it is the payout network, which `GET /v1/supply/earnings` names in `payouts.network`. A proof for one node, network or address changes no other. `rentdev-node payout --address` does all of this for you.

You only ever see your own nodes. Another account's node answers `not_found`.

## Rates, the take and the holdback

You set an ask per vCPU-hour and per GiB-hour. Customers pay your ask divided by (1 − take). The take is 20%. An ask of $0.02 per vCPU-hour and $0.0025 per GiB-hour becomes $0.025 and $0.003125 for the customer. Your share of each charge is the charge times (1 − take), rounded down to the micro-dollar.

Billing runs on the platform's clock. A lease is billed only for time your node signed for as running, in beats no more than 180 seconds apart, and that the platform saw alive. A gap longer than that is never billed, even if the sandbox comes back. Usage your node reports never raises a bill. Rates lock when a lease starts.

Your share accrues when a lease ends. It becomes releasable 14 days later, or 120 days later when the paying account topped up by card in the 120 days before. That is how long a card payment can be disputed.

Earnings freeze after a liveness mismatch on the node, or when the paying account disputes a card charge. Frozen earnings are kept, never deleted.

Earnings accrue in your ledger (`GET /v1/supply/earnings`, or `rent_supply_earnings`).

## Getting paid

Payouts are not switched on in this environment. Your earnings accrue and wait in your ledger; nothing is sent until payouts are switched on. This is how they work then.

- Each node is paid on its own, to the address registered on that node. Once a day, one payout takes the node's releasable earnings. An hourly run makes the payouts, so a node that one run does not reach is paid by a later one.

- A node with no registered address, or with one on another network than the payout network, is not paid: its earnings wait, releasable, until an address is registered. The earnings' `unaddressed_micros` is that part.

- Register the address when you enrol: `rentdev-node up --payout-address <0x…>`, or `payout` in the enrolment body. An enrolment asks for no proof.

- Register or change it later with `rentdev-node payout --address <0x…>`, on a machine that holds the account's agent key, or with `--agent-key <file>`. It takes a nonce, signs the proof for that address and network, and changes the node. Over the API it is `PATCH /v1/supply/nodes/{id}` with `payout` and `signed_nonce`, as under Managing nodes above, or `rent_supply_update_node`.

- `rentdev-node payout` alone shows the node's address and network, the releasable, in-payout and paid totals, and the node's latest payouts.

- A payout cannot be reversed, and once made it is not sent again to another address. Register the right address before the next payout takes the earnings.

- Holdbacks and freezes are as above: only releasable earnings that carry no freeze are paid, and frozen earnings are never paid while frozen.

A payout is in one of three states:

| State | What it means |
| --- | --- |
| `open` | Its amount is fixed and the transfer is sent or about to be. Its earnings count as `in_payout`. |
| `paid` | The finalized chain shows the transfer made. Its earnings count as `paid_out`. |
| `void` | The finalized chain shows its last authorization expired unused, so nothing was sent. Its earnings are freed, and a later payout takes them. |

`GET /v1/supply/payouts`, or `rent_supply_payouts`, lists your payouts, newest first, or only one node's with `node=<node id>`. Each carries its amount, `from`, `to`, `nonce`, `transaction` once known, and `verify`: `{ to, data }`. To check a payout without taking our word for it, send `verify` as an `eth_call` to any RPC of the network: the token answers 1 once the payout landed, 0 before.

## Where customer traffic comes from

Each customer request says which partner nodes may run it. It sets `routing.partners`; on the E2B-compatible API it sets the metadata key `rent.dev/partners`. The default is `active`: a request that names nothing may be placed on an active node, scored like any other endpoint, and a node on probation gets customer leases only from requests that set `all`.

- `none`: never placed on partner nodes.

- `active`: may be placed on active partner nodes.

- `all`: may be placed on active nodes and on nodes on probation.

So the time to your first customer lease depends on demand. All partner nodes together share one exploration slice of 2% of placements, split evenly by account and then by node. Running many nodes under one account does not win you more of it.

A node priced above rent.dev's own list rate for a sandbox is never explored. The node view's `placement` block shows your price against list, whether the node is explorable, and why not.

## Audits

The platform buys leases from every node at unannounced times and pays for them like any customer: your own rate, capped at rent.dev's list rate. They arrive at random, at least 1 a day, or 10% of your customer leases per day over the last 7 days if that is more. They run ordinary developer work and check the results.

A node on probation is also probed every 6 hours. A failed check counts against a node only when the same check passed on our own nodes at the same time, so an outage on our side never suspends you.

## What the checks prove

A pass shows that a node starts, sizes and ends sandboxes as it claims. Every answer comes through software you control, so the checks catch bugs and outages and make cheating cost more than it pays. They are not cryptographic proof.

Whoever operates a node can read what runs on it, which is why a customer can keep any request off partner nodes with `none`.

## Keys and signatures

- The node key lives in `/etc/rentdev-node/node.json`, readable by root only. The node signs every request with it, and the platform refuses one more than 300 seconds off its clock, or with a sequence number already used.

- The account's agent key is in `/etc/rentdev-node/account.json` when `up` made the account or was given `--agent-key`. The CLI uses it to get a `supply` token; the node never signs with it.

- The platform signs every request it sends the node. The node's gate checks the signature, the node's own hostname, a 120-second window and a fresh nonce, and refuses anything else before it reaches your backend. It fetches the platform keys from `GET https://api.rent.dev/v1/supply/keys`.

- The gate listens on 127.0.0.1 only. The node needs outbound access, no inbound port.

## Leaving

`rentdev-node down` stops the node and removes what `up` added to the machine, `/usr/local/bin/rentdev-node` included. It keeps the node's key and the account's agent key, so a later `up` resumes the same node. On a host that `up` goes through the image again.

`rentdev-node down --revoke` also revokes the node: the platform removes its link, and the node's key goes with it. The account's agent key always stays. A node claimed with a token made elsewhere is revoked from the account that enrolled it, with `POST /v1/supply/nodes/{id}:revoke` or `rent_supply_revoke_node`.

Earnings already accrued stay in your ledger.
