# SecretVM REST API for Agents (x402)

## Agent API Flow

This document describes how an agent that owns an EVM wallet calls the API to top up, check balance, create a VM, and read VM status.

### Authentication (x-agent headers)

Every agent request must include:

* `x-agent-address`: EVM wallet address.
* `x-agent-signature`: signature of the request hash.
* `x-agent-timestamp`: unix timestamp string (milliseconds preferred).

#### Request hash and signature

1. `method` = uppercase HTTP method (e.g. `GET`).
2. `path` = request path only (e.g. `/api/agent/balance`, no query string).
3. `body` = exact request body string (empty for GET). For JSON, use stable key ordering so the signed string matches the transmitted body.
4. `timestamp` = unix time string.
5. `payload` = `${method}${path}${body}${timestamp}`.
6. `request_hash` = SHA-256 hex of `payload`.
7. `signature` = `signMessage` of the hash bytes.

The server stores request hashes and rejects replays, so each request must use a fresh timestamp.

#### Multipart signature for create-vm

`POST /api/vm/create` is `multipart/form-data`. Instead of signing the raw multipart body, sign a stable JSON string:

```
{
  "fields": { ...form fields... },
  "file": {
    "fieldname": "dockercompose",
    "originalname": "docker-compose.yml",
    "mimetype": "application/x-yaml",
    "size": 123,
    "sha256": "<sha256 hex of file bytes>"
  }
}
```

Use sorted keys (stable stringify) to match server verification. This is the same structure used by the reference script.

### Typical agent flow

1. Top up (x402): `POST /api/agent/add-funds`.
2. Check balance: `GET /api/agent/balance`.
3. Create VM: `POST /api/vm/create`.
4. Poll status: `GET /api/agent/vm/:id`.

### Endpoint details

#### POST /api/agent/add-funds (top up)

Auth required.

**x402 payment (USDC)**

* Call with `amount_usdc` in the JSON body or query.
* If payment is required, server returns 402 with payment details.
* Retry with the `payment-signature` (or `x-payment`) header generated by your x402 client.
* On success, response includes:

```
{ "balance": "<minor units string>", "payment_method": "x402" }
```

#### GET /api/agent/balance

Auth required.

Response:

```
{ "balance": "<minor units string>" }
```

#### POST /api/vm/create (create VM)

Auth required.

`multipart/form-data`:

Required fields:

* `name`
* `vmTypeId`
* `dockercompose` file

Optional fields (passed as form fields):

* `inviteCode`
* `secrets_plaintext`, `secrets_encrypted`, `secrets_key`
* `docker_credentials_encrypted`, `docker_credentials_key`
* `fs_persistence`
* `custom_domain`
* `skip_launch`
* `description`
* `environment`
* `private`
* `dev_token`
* `skip_attest`
* `kms_provider`
* `platform`
* `cloudflareApiKey`
* `eip8004_registration` (JSON string)

Notes:

* Agent requests must have a balance >= `AGENT_MIN_BALANCE` (default 100 = 0.0001 USDC with 6 decimals).
* `upgradeability` is currently forced to `true` for create requests.
* `skip_attest` defaults to `"1"` for agent requests if omitted.

Response (subset):

```
{
  "id": "...",
  "name": "...",
  "created_at": "...",
  "updated_at": "...",
  "vm_type": "...",
  "vm_uid": "...",
  "vmDomain": "...",
  "skip_launch": false
}
```

#### GET /api/agent/vm/:id (VM status)

Auth required.

Response:

```
{
  "id": "...",
  "name": "...",
  "status": "...",
  "vmDomain": "...",
  "vmId": "...",
  "vmUid": "...",
  "created_at": "...",
  "updated_at": "..."
}
```

404 if the VM does not exist or does not belong to the agent.

### Examples

The header values are per-request. Recompute the signature whenever the method, path, body, or timestamp changes.

### Base URL

* `AGENT_BASE_URL=https://secretai.scrtlabs.com`

#### Build headers (Python)

```python
import hashlib
import json
import os
import time

import requests
from eth_account import Account
from eth_account.messages import encode_defunct

def stable_stringify(value):
    return json.dumps(value, sort_keys=True, separators=(",", ":"))

def build_headers(private_key, method, path, body):
    timestamp = str(int(time.time() * 1000))
    payload = f"{method}{path}{body}{timestamp}"
    request_hash = hashlib.sha256(payload.encode()).hexdigest()
    message = encode_defunct(hexstr=request_hash)
    signature = Account.sign_message(message, private_key).signature.hex()
    if not signature.startswith("0x"):
        signature = f"0x{signature}"
    address = Account.from_key(private_key).address
    return {
        "x-agent-address": address,
        "x-agent-signature": signature,
        "x-agent-timestamp": timestamp,
    }

def build_create_vm_headers(private_key, fields, file_bytes, file_name, mime):
    meta = {
        "fieldname": "dockercompose",
        "originalname": file_name,
        "mimetype": mime,
        "size": len(file_bytes),
        "sha256": hashlib.sha256(file_bytes).hexdigest(),
    }
    body = stable_stringify({"fields": fields, "file": meta})
    return build_headers(private_key, "POST", "/api/vm/create", body)

def create_vm(private_key, base_url, fields, file_bytes, mime):
    headers = build_create_vm_headers(private_key, fields, file_bytes, "docker-compose.yml", mime)
    return requests.post(
        f"{base_url}/api/vm/create",
        data=fields,
        files={"dockercompose": ("docker-compose.yml", file_bytes, mime)},
        headers=headers,
    )

def get_x402_payment_signature(challenge_json):
    sig = os.environ.get("X402_PAYMENT_SIGNATURE")
    if not sig:
        raise SystemExit("Set X402_PAYMENT_SIGNATURE from your x402 client")
    return sig

def topup_x402(private_key, base_url, amount_usdc):
    payload = {"amount_usdc": str(amount_usdc)}
    body = stable_stringify(payload)
    headers = build_headers(private_key, "POST", "/api/agent/add-funds", body)
    headers["Content-Type"] = "application/json"

    res = requests.post(f"{base_url}/api/agent/add-funds", data=body, headers=headers)
    if res.status_code == 402:
        payment_sig = get_x402_payment_signature(res.json())
        headers["payment-signature"] = payment_sig
        res = requests.post(f"{base_url}/api/agent/add-funds", data=body, headers=headers)
    return res

private_key = os.environ["AGENT_PRIVATE_KEY"].strip()
if not private_key.startswith("0x"):
    private_key = "0x" + private_key

base_url = os.environ.get("AGENT_BASE_URL", "https://secretai.scrtlabs.com").rstrip("/")
fields = {"name": os.environ["AGENT_VM_NAME"], "vmTypeId": os.environ["AGENT_VM_TYPE_ID"]}
amount_usdc = os.environ.get("AMOUNT_USDC", "1")

mime = os.environ.get("AGENT_DOCKER_COMPOSE_MIMETYPE", "application/x-yaml")
compose = """services:
  app:
    image: nginx:alpine
"""
file_bytes = compose.encode()

res = create_vm(private_key, base_url, fields, file_bytes, mime)
if res.status_code == 402 and "Insufficient balance" in res.text:
    print("Insufficient balance. Topping up via x402...")
    topup = topup_x402(private_key, base_url, amount_usdc)
    print("topup status:", topup.status_code)
    print("topup response:", topup.text)
    topup.raise_for_status()
    res = create_vm(private_key, base_url, fields, file_bytes, mime)

print("create status:", res.status_code)
print("create response:", res.text)
```

#### Balance (curl)

```bash
AGENT_BASE_URL = https://secretai.scrtlabs.com

curl -X GET "$AGENT_BASE_URL/api/agent/balance" \
  -H "x-agent-address: $AGENT_ADDRESS" \
  -H "x-agent-signature: $AGENT_SIGNATURE" \
  -H "x-agent-timestamp: $AGENT_TIMESTAMP"
```

#### Top up (curl, x402)

```bash
AGENT_BASE_URL = https://secretai.scrtlabs.com
body='{"amount_usdc":"1"}'

curl -X POST "$AGENT_BASE_URL/api/agent/add-funds" \
  -H "Content-Type: application/json" \
  -H "x-agent-address: $AGENT_ADDRESS" \
  -H "x-agent-signature: $AGENT_SIGNATURE" \
  -H "x-agent-timestamp: $AGENT_TIMESTAMP" \
  --data "$body"
```

If the response is 402, retry with the x402 `payment-signature` (or `x-payment`) header returned by your x402 client:

<pre class="language-bash"><code class="lang-bash">curl -X POST "$AGENT_BASE_URL/api/agent/add-funds" \
  -H "Content-Type: application/json" \
  -H "x-agent-address: $AGENT_ADDRESS" \
  -H "x-agent-signature: $AGENT_SIGNATURE" \
<strong>  -H "x-agent-timestamp: $AGENT_TIMESTAMP" \
</strong>  -H "payment-signature: $X402_PAYMENT_SIGNATURE" \
  --data "$body"
</code></pre>

#### VM status (curl)

```bash
curl -X GET "$AGENT_BASE_URL/api/agent/vm/$AGENT_VM_ID" \
  -H "x-agent-address: $AGENT_ADDRESS" \
  -H "x-agent-signature: $AGENT_SIGNATURE" \
  -H "x-agent-timestamp: $AGENT_TIMESTAMP"
```

#### Create VM (curl)

For multipart, the signature must be computed from the fields + file metadata shown in "Multipart signature for create-vm". The easiest path is to reuse `scripts/agent-create-vm.js`, but a raw curl request looks like:

```bash
curl -X POST "$AGENT_BASE_URL/api/vm/create" \
  -H "x-agent-address: $AGENT_ADDRESS" \
  -H "x-agent-signature: $AGENT_SIGNATURE" \
  -H "x-agent-timestamp: $AGENT_TIMESTAMP" \
  -F "name=my-vm" \
  -F "vmTypeId=TYPE_ID" \
  -F "dockercompose=@docker-compose.yml;type=application/x-yaml"
```
