Router API reference

Router turns a request for capabilities into a cloud VM on the cheapest eligible provider, billed at that provider’s raw cost. Everything below is a live endpoint under https://router.reachpad.dev/v1.

Quickstart

The base URL is https://router.reachpad.dev/v1. Every response, success or error, carries an X-Request-Id of the form rp_rq_.... It is minted per request, never read from an inbound header, and is the id to quote in a support report.

You need credits before a machine will provision. Mint an empty account and its key, top that key up, then create a machine:

$ export BASE=https://router.reachpad.dev/v1 # mint an account and its one key (shown once)$ curl -sS -X POST $BASE/accounts -H 'Content-Type: application/json' -d '{}' {"id":"cus_...","api_key":"rp_key_..."} $ export KEY=rp_key_... # create a machine$ curl -sS -X POST $BASE/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}' {"id":"rp_vm_abc123def4","status":"ready",  "host":"rp_vm_abc123def4.reachpad.dev","provider":"daytona",  "rate_per_hour":"0.165924","expires_at":"..."}

An empty account cannot fund a machine, so a create against it answers 402 until you top up. The 402 also carries a USDC payment block, so you can pay and provision in one retry without ever calling /accounts first. See paying.

Keys and accounts

Requests authenticate with a bearer key: Authorization: Bearer rp_key_.... Keys are hashed at rest and shown exactly once, in the response that minted them. A present but invalid key is always a 401; an absent key falls through to the payment door on the routes that accept one.

RouteWhat it does
POST /v1/accountsMint a fresh empty account and its one key, { id, api_key }, returned once. Open by default; set an account-mint token to require a bearer.
GET /v1/creditsThe statement: current balance, every deposit, every charge.

An account has one key. There is no second key and no scoped key in this version; rotating a key is how you revoke it, and wallet recovery is the only path that re-issues one.

Machines

You describe the machine you want in capabilities. Router filters offers to the ones that meet every requirement, sorts them by price, and provisions on the cheapest. The winning provider’s name is returned as metadata; you never send one.

FieldMeaning
cpu, ram_gb, disk_gbThe shape. An offer must meet or exceed each.
regionOne of us-west, us-east, eu-central. Matched exactly.
accessA set of access classes, default ["ssh"]. See access.
ssh_public_keyRequired for the ssh class. The key installed on the machine.
ttl_secondsHow long the machine may live. The reaper destroys it at expiry.
max_rateDrop any offer whose hourly rate is above this. Compared against the raw rate you pay.
max_spendA budget. The reaper destroys the machine when accrued spend reaches it.
client_refOptional idempotency key, unique per account. A retry carrying a ref that already names one of your machines returns that machine.

Read, list and destroy a machine by id:

# detail: adds runtime_seconds (the metered window) and live accrued cost$ curl -sS $BASE/machines/rp_vm_abc123def4 -H "Authorization: Bearer $KEY" # list, most recent first, destroyed machines included$ curl -sS $BASE/machines -H "Authorization: Bearer $KEY" # destroy: idempotent, returns the final charge$ curl -sS -X DELETE $BASE/machines/rp_vm_abc123def4 \  -H "Authorization: Bearer $KEY" {"status":"destroyed","charge":{"provider_cost":"0.000272",  "customer_total":"0.000272","runtime_seconds":12.4}}

Readiness means router’s own probe ran. A machine reports ready only after the engine sent a command to the guest and the guest answered, never because a vendor’s state string said so. A machine whose probe never answers is destroyed and the create fails, so nothing billable is left behind.

Access: ssh, exec, ports

Every machine grants a set of access classes, asked for on create as access and reported on every response. Routing filters offers on the requested set before it ranks them on price, so a class nobody can serve is a 422 naming the class rather than a machine that cannot do what you asked.

  • ssh is the default: a key you send, a reachpad user, and an <id>.reachpad.dev name. ssh_public_key is required.
  • exec is one endpoint that runs a shell command on the guest. No key, no hostname, no DNS record; it is reached through the provider.

Run a command with POST /v1/machines/:id/exec. The cmd is a shell string, and stdout and stderr come back combined:

$ curl -sS -X POST $BASE/machines/rp_vm_abc123def4/exec \  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \  -d '{"cmd":"cd app && npm test","timeout_s":300,"max_output_bytes":65536}' {"process_id":"rp_pr_...","state":"exited","exit_code":0,  "timed_out":false,"wall_time_s":4.2,"output":"...","truncated":false}

timeout_s defaults to 300 and caps at 600; at expiry the process is killed and reported with state:"killed" and exit_code:null, never a fabricated code. max_output_bytes defaults to 64 KiB and keeps the tail, where errors live. Long work needs no extra API: start it detached, tail its log, and kill it by pid, all as ordinary commands.

Expose an HTTP port with POST /v1/machines/:id/ports. The response is the provider’s own bridge URL, verbatim; reachpad is not on the data path.

$ curl -sS -X POST $BASE/machines/rp_vm_abc123def4/ports \  -H "Authorization: Bearer $KEY" -H 'Content-Type: application/json' \  -d '{"port":3000}' {"port":3000,"protocol":"http","url":"https://...","query":"","expires_at":null}

Credits and top-ups

A payment becomes a deposit, machines create charges, and the balance is deposits minus charges. One fee is taken when credits are added, and every machine then spends at the raw provider cost. GET /v1/credits returns the whole statement:

$ curl -sS $BASE/credits -H "Authorization: Bearer $KEY" {"balance":"25.00",  "deposits":[{"method":"manual","paid":"25.00","fee":"0.00",               "credited":"25.00","created_at":"..."}],  "charges":[{"machine_id":"rp_vm_...","customer_total":"0.000272",              "created_at":"..."}]}

Add credits with POST /v1/credits/topups. The body names the method and the target amount; a USDC top-up answers 402 with a payment block for that amount.

Paying with USDC over x402

USDC over x402 is the only payment method. The fee is taken once, at funding time: 5.0%, so a $100 top-up credits $95.00. Every top-up has a $1.00 minimum, below which a payment costs more in processor fees than it is worth.

The USDC method is the x402 exact scheme on Base mainnet, gasless for the payer. A top-up, or a create with no funds, answers 402 carrying the accepts block; you retry the same request with a PAYMENT-SIGNATURE header to settle. The order is verify, settle, credit, then provision:

# a create with no account and no funds$ curl -sS -X POST $BASE/machines -H 'Content-Type: application/json' \  -d '{"cpu":2,"ram_gb":4,"disk_gb":8,"region":"us-west","access":["exec"]}' {"error":"payment_required","amount":"1.00","x402Version":2,  "accepts":[{"scheme":"exact","network":"eip155:8453",              "amount":"1000000","payTo":"0x...","maxTimeoutSeconds":120}]} # retry the same request carrying the authorization$ curl -sS -X POST $BASE/machines -H 'PAYMENT-SIGNATURE: <authorization>' \  -H 'Content-Type: application/json' -d '{ ...the same body... }' {"id":"rp_vm_...","status":"ready","api_key":"rp_key_..."}

The wallet that pays is the account. On the first payment from a wallet, an account is created and its api_key is returned once. Every later payment from that wallet credits the balance and returns no key, so an agent can pay per machine, or fire parallel payments, without ever rotating the key it is holding. A replayed authorization never provisions twice.

Wallet recovery

Prove you own the wallet, get a new key. This is the only path that re-issues a key: the first payment shows it once, and no later payment shows it again. Ask for a challenge, sign it, and post the signature back.

# 1. get a single-use challenge for the wallet$ curl -sS "$BASE/credits/recovery?wallet=0xabc..." {"wallet":"0xabc...","nonce":"...","expires_at":"...",  "message":"router.reachpad.dev wants you to sign in with your Ethereum account:..."} # 2. post the signed message; the key is rotated and returned$ curl -sS -X POST $BASE/credits/recovery -H 'Content-Type: application/json' \  -d '{"wallet":"0xabc...","message":"<the message verbatim>","signature":"0x..."}' {"customer_id":"cus_...","wallet":"0xabc...","api_key":"rp_key_..."}

The challenge is single-use, lives five minutes, and is consumed on attempt rather than on success, so a wrong signature cannot be ground against one message. The door is bounded to ten attempts per wallet per five-minute window. Recovery verifies an EOA signature locally, so it works whether or not the payment method is live.

Budgets and reaping

Spend is metered per second from the moment the provider starts building the machine, at the provider’s raw rate, and settled to the microdollar. A two-hour machine at 0.079 per hour settles at 0.158; a sixty-second one settles at 0.001317, not rounded up to an hour.

  • A machine is destroyed when its ttl_seconds expires.
  • A machine is destroyed when its accrued spend reaches its max_spend.
  • When the account balance reaches zero, all of its live machines are destroyed at once, through the same path a TTL takes, with reason: "balance".

A balance can dip slightly negative between reaper ticks; it is recorded honestly and the next deposit absorbs it. There are no holds and no reservations, and there is no monthly bill: money flows in as credits and leaves as machines.

Errors and request ids

Errors are a JSON body with an error string, and every one carries the same X-Request-Id a success does. The codes:

StatuserrorWhen
401unauthorizedA bearer key was sent and did not resolve.
400invalid_requestThe body failed validation; the message names the field.
402payment_requiredThe account cannot fund the machine. The body names the shortfall and both ways to fix it.
404not_foundNo such machine.
409not_ready, access_not_grantedThe machine is not ready, or lacks the access class.
410destroyedThe machine is gone.
422no_eligible_providerNothing could serve the request; reason says why.
429rate_limitedToo many requests on an accountless door.
502provision_failedEvery eligible provider failed to build the machine.

These instructions as one agent-readable file: reachpad.dev/router/skill.md. The landing page is at reachpad.dev/router.