# VpsAgent

> Disposable VPS instances for AI agents. Pay via x402 (USDC on Base L2).
> Pay-on-success: charged only when the box is reachable on TCP :22.
> Failed provisions never settle — agent pays nothing.

No signup, no API key. Machine-to-machine over HTTP 402.

## Two product tracks

- **Sandbox** (Phase 2 — gated on provider account verification): `sandbox_1h` / `sandbox_6h` / `sandbox_24h` at $0.50–$5.00. 1 vCPU / 1 GB RAM / 25 GB SSD. Code execution, CI runs, browser automation, throw-away build envs.
- **Server** (live now): `server_{small,medium,large}_{1mo,12mo}` at $14.99–$449.99. 4–8 vCPU / 8–24 GB RAM / 100–300 GB SSD, 200–600 Mbit/s unmetered. EU default; other regions carry a surcharge. Persistent rentals; renewable.

## Every SKU, priced

Base price in the default region (EU); add the region surcharge below. This is the same table
`GET /catalog` returns — it is here, free, because a buyer who priced us from a single example row on
2026-08-28 got the medium and large specs wrong and bought anyway.

| SKU | USDC | Term | vCPU | RAM | Disk | Uplink | Buyable now |
|---|---|---|---|---|---|---|---|
| `sandbox_1h` | $0.50 | 1 h | 1 | 1 GB | 25 GB SSD | — | no — Phase 2 |
| `sandbox_6h` | $2.00 | 6 h | 1 | 1 GB | 25 GB SSD | — | no — Phase 2 |
| `sandbox_24h` | $5.00 | 1 d | 1 | 1 GB | 25 GB SSD | — | no — Phase 2 |
| `server_small_1mo` | $14.99 | 30 d | 4 | 8 GB | 100 GB SSD | 200 Mbit/s unmetered | yes |
| `server_small_12mo` | $149.99 | 365 d | 4 | 8 GB | 100 GB SSD | 200 Mbit/s unmetered | yes |
| `server_medium_1mo` | $22.99 | 30 d | 6 | 12 GB | 200 GB SSD | 300 Mbit/s unmetered | yes |
| `server_medium_12mo` | $229.99 | 365 d | 6 | 12 GB | 200 GB SSD | 300 Mbit/s unmetered | yes |
| `server_large_1mo` | $44.99 | 30 d | 8 | 24 GB | 300 GB SSD | 600 Mbit/s unmetered | yes |
| `server_large_12mo` | $449.99 | 365 d | 8 | 24 GB | 300 GB SSD | 600 Mbit/s unmetered | yes |

## Regions

- `EU` — European Union (free, default) — multi-country; no single country guaranteed
- `US-central` — United States (Central) (+$3–$9/mo by tier) — listed as St. Louis, unconfirmed
- `US-east` — United States (East) (+$3–$9/mo by tier) — listed as New York, unconfirmed
- `US-west` — United States (West) (+$3–$9/mo by tier) — listed as Seattle, unconfirmed
- `SIN` — Singapore (+$6–$18/mo by tier)
- `UK` — United Kingdom (+$3–$9/mo by tier) — listed as London, unconfirmed
- `AUS` — Australia (+$6–$18/mo by tier) — listed as Sydney, unconfirmed
- `JPN` — Japan (+$6–$18/mo by tier) — listed as Tokyo, unconfirmed
- `IND` — Mumbai, India (+$6–$18/mo by tier) — confirmed from inside a delivered box (2026-08-28)

Machine-readable at [`GET /regions`](https://vps.phoneagent.xyz/regions) — free, no payment. It splits what we
guarantee (`location` + `precision`) from what the location is merely listed as (`city_expected`).
Buying on a data-residency requirement? Ask via `POST /feedback` first — free, and we'll check.

## API
- [OpenAPI spec](https://vps.phoneagent.xyz/openapi.json): full machine-readable API.
- [Rendered docs](https://vps.phoneagent.xyz/docs).

## Key endpoints
- `GET /catalog` — the SKU table above as JSON (paid discovery, 0.01 USDC — the dust price is what registers us in the x402 discovery index; the same rows are free here, in `/openapi.json`, and in any unpaid 402 challenge).
- `POST /rent` — 402 → sign EIP-3009 → 200 with `{rental_id, ipv4, expires_at, root_token}`. No settle until the box is up.
- `GET /instance/{rental_id}?token=…` — poll for running flag + IPv4.
- `GET /rentals/{rental_id}` — public status snapshot; carries `ipv4` once the box boots (credentials are on `/instance`).
- `GET /rentals` — **lost your rental_id, or want to know what you own?** Every rental your wallet has paid for, with expiry, renewal price and whether your USDC balance covers it. Unsigned it answers 402 with an identification challenge (1 base unit, 60s); sign it with your x402 client and resend — we read your wallet from the signature and **never settle it**, so nothing is charged. Credentials are not returned.
- `POST /rentals/{rental_id}/renew?token=…&periods=N` — extend expires_at; stack cap 1–12. Price is the (sku, region) rate × periods. Out-of-range `periods` is refused, not clamped.
  Send `periods` **at most once**: two conflicting `?periods=` values are refused with 400 rather than guessed between, and the 200 echoes the `periods` it applied — check it against what you asked for.
  A probe with no `?token=` gets a **sample** quote at the entry SKU's rate × periods; only a tokened call is priced for your actual rental.
  A renewal normally completes within a minute, but `expires_at` **does not move until it settles** — poll
  `GET /instance/{rental_id}?token=…`, whose `renewal` block reports progress and confirms that nothing has been
  charged yet. If it never completes you are not charged at all, the rental returns to `active` so you can retry,
  and the box still dies at the original `expires_at`.
  Within **15 minutes** of expiry a renewal is **refused** with 409 `too_late_to_renew` rather than sold: there is
  not enough runway for the extension to be in place before the box is destroyed, so it would take your signature,
  extend nothing and leave you without a box. Rent a fresh one instead — the 409 carries `rent_url`.
- `GET /regions` — where each region code actually puts a box, how precisely we can pin it, and its surcharge. Free.
- `POST /feedback` — free, no auth. POST your report as raw markdown, or JSON `{message, rental_id?, token?, contact?}`. Reaches a human.

## Something wrong?

Tell us: `POST https://vps.phoneagent.xyz/feedback` with the text as the body. No key, no payment, no signup.
Most useful: anything you had to guess, anything that looked broken but wasn't, and any documentation that was wrong.

## Payment
- Network: `eip155:8453`
- Asset: USDC (`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`)
- Protocol: **x402 v2**. v1 payloads are rejected with `invalid amount:` — update your client.
- **Read the challenge from the `PAYMENT-REQUIRED` response header** (base64 of the same JSON we put in the 402
  body). The v2 reference client (`@x402/core`) only falls back to the body when `x402Version == 1` and
  otherwise throws `Invalid payment required response` — that error means you read the body, not that we are down.
- Send the signed payload back in `PAYMENT-SIGNATURE`. `X-Payment` is also accepted.
- Scheme: x402 `exact` with deferred settlement. Server calls /settle only when the box is reachable; otherwise the authorization expires unused.
- **A payment failure comes back `402` if retrying could help, `422` if it can't.** `422` +
  `insufficient_funds` means your wallet's on-chain USDC balance is below the quoted price — the body names
  `balance_usdc`, `required_usdc` and `shortfall_usdc`. It is terminal and carries no `accepts[]`: fund the
  wallet, then retry. Do not loop on it. Every other payment failure (bad signature, spent nonce, a balance we
  couldn't read) stays `402`, because those may succeed on a retry.
- Nothing is charged on a failed payment. Quotes are free: an unpaid request to any paid route returns the
  priced challenge without committing you to anything.
