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.
| Route | What it does |
|---|---|
POST /v1/accounts | Mint an account and its one key, { id, api_key, balance }, returned once. Behind a mint token in production: sign in at reachpad.dev/router/account to get an account, which starts with $5 of free credits. |
GET /v1/credits | The 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. A wallet account gets a new one two ways: issue_key on a payment, or wallet recovery.
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.
| Field | Meaning |
|---|---|
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 of access classes, default ["ssh"]. See access. |
ssh_public_key | Required for the ssh class. The key installed on the machine. |
ttl_seconds | How long the machine may live. The reaper destroys it at expiry. |
max_rate | Drop any offer whose hourly rate is above this. Compared against the raw rate you pay. |
max_spend | A budget. The reaper destroys the machine when accrued spend reaches it. |
client_ref | Optional 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.
sshis the default: a key you send, areachpaduser, and an<id>.reachpad.devname.ssh_public_keyis required.execis 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 payment method the API itself takes; a card pays through a signed-in account at reachpad.dev/router/account, which starts with $5 of free credits. The fee is taken once, at funding time: USDC 5.0%, so a $100 top-up credits $95.00; card 5.5% with an $0.80 minimum, shown before you pay. 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 that carries that key credits the balance and returns no key, so an agent can pay per machine, or fire those payments in parallel, without ever rotating the key it is holding. A replayed authorization never provisions twice.
Make the first payment from a wallet on its own and wait for it. Two anonymous first payments from the same new wallet, in flight together, can both settle: the 409 below catches the sequential case only. One of them then holds a machine nothing can reach until the key is re-issued.
A wallet that has an account but no longer has its key asks a payment to re-issue one. Send "issue_key": true in the body of a create or a top-up that carries a payment. The payment settles, the key is rotated, and the new key comes back in that response. The signature on the payment proves the wallet, so there is no separate message to sign.
# pay, and ask the payment to re-issue this wallet's key$ curl -sS -X POST $BASE/machines -H 'PAYMENT-SIGNATURE: <authorization>' \ -H 'Content-Type: application/json' \ -d '{ ...the same body..., "issue_key": true }' {"id":"rp_vm_...","status":"ready","api_key":"rp_key_...", "key_issued":"rotated"}
Every response that settled a payment carries key_issued: created on the payment that made the account, rotated when that payment re-issued the key, and false when the response carries no key. Rotation is revocation, so the old key stops working.
- Sign a flagged payment fresh. The authorization’s
validBeforemust be within 10 minutes of now and must not be zero; a stale or unbounded one is a400. Payments without the flag are unaffected. - Send flagged payments one at a time. The last rotation wins, and the key an earlier response returned is already dead.
- A replay re-issues nothing. Re-presenting the same authorization after a lost response returns that original response, with
key_issued: falseandkey_issued_reason: "replay". A key comes back through a new payment carrying the flag, or through recovery. - Flagged payments and recovery attempts share one budget per wallet: 10 in any 5 minutes. Past it the answer is
429, before anything settles.
A payment from a wallet that already has an account, sent with no bearer key and no issue_key, answers 409 account_exists before the authorization reaches the payment processor, so no money moves. Send the account’s key as a bearer, or add "issue_key": true. Asking for issue_key on a request that carries no payment is a 400.
The flag makes an authorization a credential. An unspent one presented with "issue_key": true inside its window re-issues the account’s key to whoever sent it, so keep PAYMENT-SIGNATURE bytes out of logs and shared output, and keep validBefore short.
Wallet recovery
Prove you own the wallet, get a new key. Use this door when the wallet is not making a payment right now; a payment carrying "issue_key": true re-issues the key too. 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, a budget it shares with payments carrying issue_key. 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_secondsexpires. - 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:
| Status | error | When |
|---|---|---|
| 401 | unauthorized | A bearer key was sent and did not resolve. |
| 400 | invalid_request | The body failed validation; the message names the field. Also a flagged payment whose validBefore is stale or zero. |
| 402 | payment_required | The account cannot fund the machine. The body names the shortfall and both ways to fix it. |
| 404 | not_found | No such machine. |
| 409 | not_ready, access_not_granted | The machine is not ready, or lacks the access class. |
| 409 | account_exists | The paying wallet already has an account and the request sent no key. Nothing settled. Send that key, or add "issue_key": true, which re-issues the key to whoever holds the authorization. |
| 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 an accountless door, or more than 10 key re-issues asked for one wallet in 5 minutes. |
| 502 | provision_failed | Every 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.