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 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/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. 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.

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 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 validBefore must be within 10 minutes of now and must not be zero; a stale or unbounded one is a 400. 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: false and key_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_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. Also a flagged payment whose validBefore is stale or zero.
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.
409account_existsThe 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.
410destroyedThe machine is gone.
422no_eligible_providerNothing could serve the request; reason says why.
429rate_limitedToo many requests on an accountless door, or more than 10 key re-issues asked for one wallet in 5 minutes.
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.