---
name: reachpad-router
version: 1.1.0
description: >-
  Provision and drive disposable cloud VMs over plain HTTP. Reach for this when
  a task needs a real Linux machine for minutes or hours: run a test suite or a
  build, execute untrusted code, host a dev server, or get an ssh box, without
  picking a cloud provider. POST cpu, ram, disk and region to
  router.reachpad.dev; the machine that fits provisions on whichever provider
  prices it best, billed per second at raw provider cost, and is destroyed on a
  TTL, a budget, or a DELETE. Pay with USDC over x402. Not for
  long-lived production hosting.
---

# reachpad router: instructions for an agent

reachpad router is a cloud VM API. You ask for capabilities (CPU, RAM, disk,
region) and it keeps the provider offers that fit, provisions the best-priced
one, meters it at that provider's raw per-second cost, and destroys it on your
TTL, budget, or a DELETE. You never name a provider.

- Base URL: `https://router.reachpad.dev/v1`
- Requests and responses are JSON.
- Every response carries `X-Request-Id: rp_rq_...`. Quote it in any support report.
- Money is decimal USD strings. USDC has 6 decimals and maps 1:1 to these amounts.
- This file lives at `https://reachpad.dev/router/skill.md`. If you are asked
  to set it up, save it to your skill directory (for example
  `.claude/skills/reachpad-router/SKILL.md`) so it loads in future sessions,
  and re-fetch it when a saved copy and the API disagree.

## Get an identity

You need credits before a machine provisions. Three ways in:

1. **You already have a key.** Send `Authorization: Bearer rp_key_...` on every request.
2. **Mint a key, then top up.** `POST /v1/accounts` returns `{ "id", "api_key" }` once. Store the key; it is shown once and cannot be re-shown.
3. **Pay with no account.** Create a machine or a top-up with no key. You get a `402` carrying a payment block; pay it and the paying wallet becomes the account (see USDC below).

## Create a machine

```bash
curl -sS -X POST https://router.reachpad.dev/v1/machines \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"cpu":2,"ram_gb":4,"disk_gb":8,"region":"us-west",
       "ssh_public_key":"ssh-ed25519 AAAA... you@host",
       "ttl_seconds":3600,"max_spend":"1.50"}'
# 201 -> {"id":"rp_vm_...","status":"ready","host":"rp_vm_....reachpad.dev",
#         "ssh_user":"...","provider":"...","rate_per_hour":"...",
#         "expires_at":"..."}
# provider and rate depend on routing: the best-priced offer that fits wins.
```

Request fields:

- `cpu`, `ram_gb`, `disk_gb`: the shape. An offer must meet or exceed each.
- `region`: one of `us-west`, `us-east`, `eu-central`. Matched exactly.
- `access`: a set, default `["ssh"]`. Use `["exec"]` for command execution with no key or hostname, or `["ssh","exec"]` for both.
- `ssh_public_key`: required when `access` includes `ssh`. An `ssh-*` or
  `ecdsa-*` OpenSSH key; FIDO `sk-*` keys are rejected.
- `ttl_seconds`: the machine is destroyed at expiry.
- `max_rate`: drop any offer whose hourly rate is above this.
- `max_spend`: the machine is destroyed when accrued spend reaches this budget.
  Also a routing filter: offers that cannot run at least 300 seconds inside
  the budget are dropped, and a budget no offer fits answers 422, not 402.
- `client_ref`: optional idempotency key, unique per account. A retry carrying a ref that already names one of your machines returns that machine instead of provisioning again. Use it so a timeout retry never buys a second machine.

Read, list and destroy:

```bash
curl -sS https://router.reachpad.dev/v1/machines/$ID -H "Authorization: Bearer $KEY"
curl -sS https://router.reachpad.dev/v1/machines -H "Authorization: Bearer $KEY"
curl -sS -X DELETE https://router.reachpad.dev/v1/machines/$ID -H "Authorization: Bearer $KEY"
# DELETE is idempotent and returns the final charge.
```

## Pay with USDC (x402, no account needed)

The USDC method is the x402 `exact` scheme on Base mainnet (`eip155:8453`), gasless
for the payer.

1. Send the create (or a top-up) with no funds. You get `402`:

```json
{"error":"payment_required","amount":"1.00","x402Version":2,
 "accepts":[{"scheme":"exact","network":"eip155:8453","amount":"1000000",
             "asset":"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
             "payTo":"0x...","maxTimeoutSeconds":120,
             "extra":{"name":"USD Coin","version":"2"}}]}
```

Sign against the EIP-712 domain `extra` names. The same payment block also
rides base64-encoded on the `PAYMENT-REQUIRED` response header, its canonical
location.

2. Retry the **same** request with a `PAYMENT-SIGNATURE` header carrying the authorization:

```bash
curl -sS -X POST https://router.reachpad.dev/v1/machines \
  -H "PAYMENT-SIGNATURE: <authorization>" -H "Content-Type: application/json" \
  -d '{ ...the same body... }'
# 201 -> {"id":"rp_vm_...","status":"ready","api_key":"rp_key_..."}
```

The wallet that pays is the account. `api_key` is returned **only** on the first
payment from a wallet (the one that creates the account); store it. Later payments
from the same wallet credit the balance and return no key, so you can pay per
machine, or fire parallel payments, without rotating the key you hold. A replayed
authorization never provisions twice. This holds even when the request also
carried a bearer key: an unknown paying wallet creates a fresh account and the
machine belongs to it, not to the key you sent. Already holding an account, top
it up rather than paying a create from a new wallet.

USDC over x402 is the only payment method. Every top-up has a $1.00 minimum,
and the one fee is taken at top-up: 5.0%. Everything a machine spends after
that is raw provider cost.

## Run a command (exec machines)

```bash
curl -sS -X POST https://router.reachpad.dev/v1/machines/$ID/exec \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"cmd":"cd app && npm test","timeout_s":300,"max_output_bytes":65536}'
# 200 -> {"state":"exited","exit_code":0,"wall_time_s":4.2,"output":"...","truncated":false}
```

`cmd` is a shell string; stdout and stderr come back combined. `timeout_s` defaults
to 300 and caps at 600. For long work, detach it and poll: run
`nohup <cmd> > /tmp/job.log 2>&1 & echo $!` to start, `tail -n50 /tmp/job.log` to
check, `kill <pid>` to stop. One provider caveat: when the response says
`"provider":"blaxel"`, a detached process loses its outbound network about 15
seconds after the exec that started it returns, so keep long outbound-holding
work (clones, queue consumers) inside one exec there.

## Expose a port

```bash
curl -sS -X POST https://router.reachpad.dev/v1/machines/$ID/ports \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"port":3000}'
# 201 -> {"port":3000,"protocol":"http","url":"https://...","query":"","expires_at":null}
```

Returns the provider's own bridge URL. HTTP and WebSocket only; raw TCP is the ssh
class's job.

## SSH (ssh machines)

Connect as the `ssh_user` and `host` from the create response:

```bash
ssh <ssh_user>@<host>
# Modal machines publish sshd on a non-standard port and carry an ssh_port field:
ssh -p <ssh_port> reachpad@<host>
```

## Budgets and cleanup

- Set `ttl_seconds` and `max_spend` on every machine.
- The reaper destroys a machine when its TTL expires, when its accrued spend
  reaches `max_spend`, or destroys all of your live machines at once when the
  balance reaches zero. A machine stuck provisioning, whose readiness probe
  never answers, is torn down too, and that failed create is not billed.
- Spend is metered per second at the provider's raw cost and settled to the
  microdollar. A machine that ran a minute is charged for a minute.
- `GET /v1/credits` returns `{ "balance", "deposits", "charges" }`.

## Recover a lost key

```bash
curl -sS "https://router.reachpad.dev/v1/credits/recovery?wallet=0x..."
# -> a challenge message to sign with the wallet
curl -sS -X POST https://router.reachpad.dev/v1/credits/recovery \
  -H "Content-Type: application/json" \
  -d '{"wallet":"0x...","message":"<the message verbatim>","signature":"0x..."}'
# -> {"api_key":"rp_key_..."}
```

Rotation is revocation: the new key works and the old one stops. This is the only
path that re-issues a key, and it exists only for wallet-funded accounts: a key
minted at `POST /v1/accounts` with no wallet payment behind it cannot be
recovered, so store it.

## Errors

Errors are JSON with an `error` string and the same `X-Request-Id` a success
carries.

- `401 unauthorized`: a missing or unresolvable key. Only the payment doors
  treat a missing key as anonymous; every other keyed route 401s without one.
- `400 invalid_request`: the body failed validation; `message` names the field.
- `402 payment_required`: no funds; the body names the amount and the payment block.
- `404 not_found`: no such machine.
- `409`: `not_ready`, `access_not_granted`, `payment_in_flight`,
  `create_interrupted` or `account_conflict`; the body names which.
- `410 destroyed`: the machine is gone.
- `422 no_eligible_provider`: nothing could serve the request; `reason` says why.
- `429 rate_limited`: too many requests on a door that answers without a key.
- `501 not_implemented`: the winning offer lacks the feature (ports on a
  provider with no bridge).
- `502 provision_failed`: every eligible provider failed to build the machine.
- `500 internal_error`: unexpected; report it with the `X-Request-Id`.

Retry safely: pass `client_ref` on a create, or re-present the same payment. Both
return the machine you already have instead of making a second one.

## Rules for agents

1. **Set `ttl_seconds` and `max_spend` on every create**, sized to the job.
   The TTL defaults to 14400 (4 hours); there is no default budget.
2. **Pass `client_ref` on every create.** A timed-out retry then returns the
   machine you already bought instead of provisioning a second one.
3. **DELETE the machine the moment the job is done.** The TTL is the backstop,
   not the plan, and DELETE is idempotent and returns the final charge.
4. **Keep each exec under its `timeout_s`** (default 300, max 600). Start long
   work detached (`nohup <cmd> > /tmp/job.log 2>&1 & echo $!`) and poll its
   log with further execs.
5. **Keep `rp_key_...` in an environment variable**, never in code, output or
   logs. It is shown once; a wallet account can re-issue it through recovery,
   a minted account cannot.
6. **On `422 no_eligible_provider`, change the ask** instead of retrying the
   same request: a smaller shape, another region, fewer access classes, a
   bigger `max_spend` or `max_rate`, or a shorter `ttl_seconds` (some
   offers cap TTL below the 14400 default).
7. **On `402`, pay it or surface it.** Retry the same body carrying the
   payment, or report the amount to the user. Never loop on an unpaid 402, and
   back off on `429`.
8. **Report failures with their `X-Request-Id`.** It is the join key for the
   request's whole trail on the server side.

The human reference for this API is at https://reachpad.dev/router/docs.
