# osmoda Spawn - Agent Skill Document

> Read this if you are an AI agent. This tells you everything you need to
> spawn and operate osmoda servers programmatically.
>
> For human-readable docs with interactive examples, see: https://spawn.os.moda/docs

Version: **1.3.0** · Last updated: 2026-05-06

Source:        https://spawn.os.moda/SKILL.md
Skill page:    https://spawn.os.moda/skill
Human Docs:    https://spawn.os.moda/docs
Agent Card:    https://spawn.os.moda/.well-known/agent-card.json
OpenAPI Spec:  https://spawn.os.moda/api/v1/docs
GitHub:        https://github.com/bolivian-peru/os-moda
Telegram:      https://t.me/osmodasystems
Discord:       https://discord.gg/G7bwet8B

---

## What is osmoda

osmoda is an AI-native NixOS distribution. When you spawn an osmoda server, you
get a dedicated virtual machine with an AI agent already running on it. The
agent has root access through **92 structured tools** exposed by the MCP bridge,
backed by 10 daemons. It can install software, manage services, deploy
apps, monitor health, auto-fix problems, and communicate with you over
WebSocket.

The server is not a container. It is a real VM with its own IP, its own NixOS
install, its own AI brain. You SSH into it, or you talk to its agent, or both.

**10 daemons + 1 CLI** (9 Rust daemons + 1 TypeScript gateway + 1 Rust CLI):

- `agentd` (Rust): system bridge (health, query, events, memory, backups, approval gate)
- `osmoda-keyd` (Rust): crypto wallet (ETH + SOL, policy-gated signing)
- `osmoda-watch` (Rust): SafeSwitch deploys + autopilot watchers with auto-rollback
- `osmoda-routines` (Rust): background cron/event automation
- `osmoda-voice` (Rust): local STT/TTS (whisper.cpp + piper)
- `osmoda-mesh` (Rust): P2P encrypted agent-to-agent communication (Noise_XX + ML-KEM-768)
- `osmoda-mcpd` (Rust): MCP server lifecycle manager
- `osmoda-teachd` (Rust): system learning, self-optimization, auto-skill-generation
- `osmoda-egress` (Rust): localhost-only HTTP proxy with domain allowlist
- `osmoda-gateway` (TypeScript): modular agent gateway (claude-code + openclaw drivers)
- `agentctl` (Rust CLI): verify-ledger, events

**The gateway is modular** - each agent independently picks its runtime engine
(`claude-code` or `openclaw`), credential, and model. Switchable at runtime via
the dashboard, no SSH or rebuild needed.

---

## How to spawn a server

You pay with USDC via the x402 protocol on Base Sepolia testnet or Solana
Devnet. (As of May 2026, no public x402 facilitator supports mainnet - the
spawn server runs in testnet mode until that changes.) No signup, no accounts,
no API keys for the spawn flow itself.

### Step 1: Check plans

```
GET https://spawn.os.moda/api/v1/plans
```

Returns JSON with all plans, pricing, regions, and x402 payment info.

### Step 2: Spawn (triggers x402 payment)

```
POST https://spawn.os.moda/api/v1/spawn/{planId}
Content-Type: application/json
Idempotency-Key: <16-128 chars, [A-Za-z0-9_-]>   (optional but recommended)

Body (all fields optional):
{
  "region":        "eu-central",
  "ssh_key":       "ssh-ed25519 AAAA...",
  "runtime":       "claude-code",            # v1.2: or "openclaw"
  "default_model": "claude-opus-4-7",         # newest Anthropic Opus
  "credentials": [                             # v1.2: pre-seed credentials
    {
      "label":    "My Claude Pro",
      "provider": "anthropic",                # anthropic | openai | openrouter
      "type":     "oauth",                    # oauth | api_key
      "secret":   "sk-ant-oat01-..."
    }
  ],
  "ai_provider":   "anthropic",               # legacy - auto-promoted to a credential
  "api_key":       "sk-ant-api03-..."         # legacy - auto-promoted to a credential
}
```

**First call** returns `402 Payment Required`. Parse the x402 envelope for price,
USDC asset, network, and `payTo` address. Your x402 client signs + retries with
a `PAYMENT` header. On success you get `200`:

```json
{
  "order_id":    "550e8400-e29b-41d4-a716-446655440000",
  "api_token":   "osk_a1b2c3d4e5f6...",
  "plan":        "Solo",
  "price_usd":   14.99,
  "server_ip":   "1.2.3.4",
  "status":      "provisioning",
  "status_url":  "https://spawn.os.moda/api/v1/status/550e8400-...",
  "chat_url":    "wss://spawn.os.moda/api/v1/chat/550e8400-...",
  "ssh":         "ssh root@1.2.3.4",
  "message":     "Server provisioning. osmoda installs in 5-10 minutes."
}
```

**IMPORTANT:** Save the `api_token`. It is shown only once. You need it for
status checks, WebSocket chat, the v1.3.0 events stream, and token-lifecycle
endpoints.

### Idempotency (strongly recommended for programmatic use)

Pass an `Idempotency-Key` header on every spawn. It's safe across retries:

- **Same key + same body** -> returns the original response byte-for-byte with
  `Idempotent-Replayed: true` for 24 hours. **No re-charge at x402.**
- **Same key + different body** -> `409 idempotency_key_reused`.
- **No header** -> current-call semantics (first success charges, retries re-charge).

Format: 16-128 chars, `[A-Za-z0-9_-]`. Generate a new key per business-level
spawn, e.g. `myapp-2026-05-06-<uuid>`.

### Step 3: Wait for provisioning (5-10 minutes)

```
GET https://spawn.os.moda/api/v1/status/{orderId}
Authorization: Bearer osk_{token}
```

Poll every 15-30 seconds. Status transitions:
`pending` -> `provisioning` -> `running` (or `failed` / `expired`).

Basic status (no token) returns `{order_id, status, plan, created_at}`.
Full status (Bearer `osk_`) returns `server_ip`, `server_name`, `region`,
`chat_url`, `ssh`, `price_usd` too. The `provision_steps[]` array reflects
phase-level install progress; `install_error.log_tail` is set on failure.

### Step 4: Use the server

Once `status` is `running`:

- **SSH:** `ssh root@{server_ip}`
- **WebSocket chat:** `wss://spawn.os.moda/api/v1/chat/{orderId}?token=osk_{token}`
- **Live events stream (v1.3.0):** `GET /api/v1/servers/{orderId}/events` (SSE)
- **Web dashboard:** https://spawn.os.moda - Engine tab lets you swap runtimes,
  add credentials, change models per agent, all without SSH.

---

## Plans

| Plan ID      | Name  | Price    | CPU | RAM  | Disk  | Use case                  |
|--------------|-------|----------|-----|------|-------|---------------------------|
| `test`       | Solo  | $14.99   | 2   | 4GB  | 40GB  | 1 agent, light tasks      |
| `starter`    | Pro   | $34.99   | 4   | 8GB  | 80GB  | 2-4 agents, real work     |
| `developer`  | Team  | $62.99   | 8   | 16GB | 160GB | 5-10 agents, heavy loads  |
| `production` | Scale | $125.99  | 16  | 32GB | 320GB | 10-20+ agents, full fleet |

All plans include: NixOS, 10 daemons, 92 MCP tools, modular `osmoda-gateway`
with `claude-code` + `openclaw` drivers, heartbeat monitoring, web dashboard,
auto-healing watchers, the v1.3.0 unified event stream.

## Regions

| ID          | Location               |
|-------------|------------------------|
| eu-central  | Frankfurt, Germany     |
| eu-north    | Helsinki, Finland      |
| us-east     | Virginia, USA          |
| us-west     | Oregon, USA            |

Default: `eu-central`.

---

## All public endpoints

### Free (no payment, no auth)

```
GET  /api/v1/plans                     List plans + pricing + x402 info
GET  /api/v1/status/{orderId}          Basic status (full with Bearer)
GET  /api/v1/docs                      OpenAPI 3.0 spec (JSON)
GET  /.well-known/agent-card.json      A2A / ERC-8004 agent discovery
GET  /SKILL.md                         This document
GET  /skill                            Same content, rendered for browsers
```

### x402 payment required

```
POST /api/v1/spawn/test                Spawn Solo   ($14.99)
POST /api/v1/spawn/starter             Spawn Pro    ($34.99)
POST /api/v1/spawn/developer           Spawn Team   ($62.99)
POST /api/v1/spawn/production          Spawn Scale  ($125.99)
```

### Bearer `osk_` required (reseller / programmatic surface)

```
GET    /api/v1/status/{orderId}                Full status (server_ip, chat_url, ...)
GET    /api/v1/tokens/{token_id}               Token metadata (expires_at, revoked_at)
DELETE /api/v1/tokens/{token_id}               Revoke this token (returns 204)
GET    /api/v1/spec-kit/projects               Spec-driven projects across your fleet
GET    /api/v1/servers/{orderId}/events        SSE event stream (v1.3.0)
GET    /api/v1/servers/{orderId}/requests      Recent request receipts (v1.3.0)
GET    /api/v1/servers/{orderId}/requests/{request_id}   Single receipt (v1.3.0)
GET    /api/v1/servers/{orderId}/spawn-log     NDJSON event log (v1.3.1)
WS     /api/v1/chat/{orderId}?token=osk_{token}          WebSocket chat
```

### Dashboard surface (`sk_live_` Bearer or session cookie)

```
GET    /api/dashboard/servers                              List your servers
POST   /api/dashboard/servers/{id}/api-key                 Set / rotate AI API key (v1.3.0: 202 + request_id)
DELETE /api/dashboard/servers/{id}/api-key                 Remove AI API key
POST   /api/dashboard/servers/{id}/agents/{agent}/restart  Restart wedged agent (v1.2.6)
POST   /api/dashboard/servers/{id}/chat-async              Async chat (v1.2.5, returns 202)
GET    /api/dashboard/servers/{id}/chat-stream/{conv_id}   SSE chat stream (v1.2.5)
GET    /api/dashboard/servers/{id}/chat-history/{conv_id}  Cold load (v1.2.5)
GET    /api/dashboard/servers/{id}/events                  Unified SSE events (v1.3.0)
GET    /api/dashboard/servers/{id}/requests                Request history (v1.3.0)
GET    /api/dashboard/servers/{id}/requests/{request_id}   Single receipt (v1.3.0)
```

The reseller (`osk_`) and dashboard (`sk_live_`) surfaces emit identical event
shapes - only the auth differs. SDKs can target one event schema.

---

## v1.3.0 - Unified server event plane + request receipts

Every async action in osmoda now returns a typed `request_id` and emits typed
lifecycle events on a per-server SSE stream. One subscription, all actions.
Cursor-resumable, 15 s keepalive, 30 min hard timeout.

### Subscribe

```
GET /api/v1/servers/{orderId}/events?cursor=N&filter=request,state,agent,heartbeat,install
Authorization: Bearer osk_{token}
Accept: text/event-stream
```

Filter is comma-separated; matches by event-type prefix. Cursor of 0 replays
all stored events; pass the last seen `id` to resume.

### Event types

```
request.accepted     {request_id, action, accepted_at, expected_completion_within_seconds}
request.progress     {request_id, action, progress: {stage, tool?, ...}}
request.completed    {request_id, action, result}
request.failed       {request_id, action, failure: {code, message, fallback_recommendation?}}
request.superseded   {request_id, action, superseded_by}
state.changed        {field, value}                  -- e.g. has_api_key flips true
agent.wedged         {stale_min, last_heartbeat}
agent.healed         {via}                            -- 'auto_recovery' | 'manual'
heartbeat.received   {server_status, agents_count, lag_ms}
install.progress     {step, status, detail}
```

Common `failure.code` values:
`agent_silent`, `agent_disconnected`, `no_credential`, `gateway_wedged`,
`gateway_unreachable`, `ssh_pam_expired`, `ssh_restart_failed`, `timeout`,
`exception`.

Common `fallback_recommendation` values:
`add_api_key`, `restart_agent`, `wait_for_wedge_auto_restart`,
`delete_and_respawn`, `retry`.

### Action -> event flow

| Action              | request.accepted | progress events                       | terminal           |
|---------------------|------------------|---------------------------------------|--------------------|
| `chat_message`      | on POST          | tool_use, tool_result                 | completed / failed |
| `agent_restart`     | on POST          | (none)                                | completed / failed |
| `key_delivery`      | on POST          | (none)                                | completed / failed |

When a fresh action of the same kind is issued before the previous one
finishes, the older one is marked `superseded` and a `request.superseded` event
fires - useful when the user re-keys mid-flight.

### Request receipts (one-shot status)

Don't want SSE? Poll the receipt endpoint:

```
GET /api/v1/servers/{orderId}/requests/{request_id}
Authorization: Bearer osk_{token}
```

Returns:

```json
{
  "request_id":  "req_chat_message_a1b2...",
  "order_id":    "550e8400-...",
  "action":      "chat_message",
  "status":      "completed",
  "accepted_at": "2026-05-06T13:14:00.000Z",
  "completed_at":"2026-05-06T13:14:02.471Z",
  "expected_completion_within_seconds": 60,
  "progress":    null,
  "result":      { "elapsed_ms": 2471 },
  "failure":     null,
  "_links": {
    "status": "/api/v1/servers/.../requests/req_chat_message_a1b2...",
    "events": "/api/v1/servers/.../events?filter=request,state&since=0"
  }
}
```

History endpoint: `GET /api/v1/servers/{orderId}/requests?action=key_delivery&limit=10`.

### Reference TS subscriber

```ts
const es = new EventSource(
  `https://spawn.os.moda/api/v1/servers/${orderId}/events?cursor=0`,
  { withCredentials: false }
);
es.addEventListener('server', (e) => {
  const ev = JSON.parse(e.data);
  if (ev.type === 'request.completed' && ev.action === 'key_delivery') {
    /* key landed on the customer box */
  }
  if (ev.type === 'agent.wedged') {
    /* a wedge auto-restart is about to kick in */
  }
});
```

EventSource cannot send custom Authorization headers in browsers. For
`osk_` Bearer use, either (a) call from Node with the standard `eventsource`
package, (b) wrap the URL with a one-shot signed cookie endpoint, or (c) use
the dashboard surface where cookies handle auth automatically.

---

## v1.2.5 - Async chat with SSE streaming (dashboard)

Three additive endpoints replace the old REST `/chat`:

```
POST /api/dashboard/servers/{id}/chat-async                  -> 202 {conversation_id, message_id}
GET  /api/dashboard/servers/{id}/chat-stream/{conversation_id}?cursor=N
GET  /api/dashboard/servers/{id}/chat-history/{conversation_id}
```

`chat-stream` is SSE, cursor-resumable. Events: `text`, `tool_use`,
`tool_result`, `done`, `error`. Cold loads use `chat-history` (NDJSON to JSON).
Empty-reply mode emits a final `error` event with `code: agent_silent`.

Auth: `sk_live_` Bearer or session cookie (the dashboard surface).

---

## v1.2.6 - Managed agent restart

```
POST /api/dashboard/servers/{id}/agents/{agent}/restart
-> 202 {restart_id, request_id, status: 'restarting'}

GET  /api/dashboard/servers/{id}/agents/{agent}/restart/{restart_id}
GET  /api/dashboard/servers/{id}/requests/{request_id}    (preferred, v1.3.0)
```

The restart kicks `systemctl restart osmoda-gateway` over SSH and waits up to
60 s for a fresh heartbeat. Status transitions:
`restarting` -> `ready` | `timeout` | `failed`.

Use this when the agent is alive-but-not-pulling-work (chat returns
`agent_silent`, or `agent_responsive: false` on the server-list response).

---

## x402 payment protocol

x402 is an HTTP-native payment standard by Coinbase. Flow:

1. POST without payment -> `402 Payment Required` + x402 envelope in body
2. Sign the payment:
   - Base (EVM): USDC `transferWithAuthorization` (EIP-3009, gasless)
   - Solana (SVM): USDC SPL token transfer
3. Retry same POST with `PAYMENT` header
4. Facilitator verifies + settles on-chain
5. Server returns `200` with provisioned server details

**Networks currently accepted** (testnet only as of May 2026):
- `eip155:84532` Base Sepolia, USDC at `0x036CbD53842c5426634e7929541eC2318f3dCF7e`
- `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` Solana Devnet, USDC at `4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU`

**Scheme:** `exact` (pay exactly the listed price).

**Client libraries:**

- `@x402/fetch` - wraps `fetch()` with automatic 402 handling (EVM + SVM)
- Coinbase CDP SDK - built-in x402 support
- `@osmoda/client` - first-party TypeScript SDK for osmoda (handles credentials, errors, retries)

**Example with `@x402/fetch`:**

```ts
import { withPayment } from "@x402/fetch";

const fetch402 = withPayment(fetch, { wallet: myWallet });
const res = await fetch402("https://spawn.os.moda/api/v1/spawn/test", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": "myapp-2026-05-06-" + crypto.randomUUID(),
  },
  body: JSON.stringify({
    region: "eu-central",
    runtime: "claude-code",
    credentials: [{
      label: "My Claude Pro",
      provider: "anthropic",
      type: "oauth",
      secret: process.env.CLAUDE_OAUTH_TOKEN,
    }],
  }),
});
const server = await res.json();
```

---

## Authentication

Three orthogonal mechanisms, all stateless:

1. **x402 PAYMENT header** - authenticates spawn requests (you pay, you're in).
2. **Bearer `osk_{token}`** - authenticates post-spawn programmatic operations
   (status, tokens, WebSocket, v1.3.0 events + requests, spec-kit). Each token
   is scoped to one order.
3. **Bearer `sk_live_{token}` or signed session cookie** - authenticates
   dashboard operations (per-server config, chat-async, agent restart, events).
   `sk_live_` keys are user-scoped (one user, many servers).

The `osk_` token is:

- `osk_` prefix + 64 hex characters (256-bit random)
- Returned once in the spawn response
- Sent as: `Authorization: Bearer osk_{token}` (HTTP) or `?token=osk_{token}` (WS)
- Server-side stored only as SHA-256 hash + timing-safe-compared
- Has metadata: `created_at`, `expires_at` (default 1 year), `revoked_at`

**Token lifecycle:**

```
# Read your own metadata
GET https://spawn.os.moda/api/v1/tokens/{token_id}
Authorization: Bearer osk_{token}

# Revoke your own token (204 on success; subsequent calls return 401 token_revoked)
DELETE https://spawn.os.moda/api/v1/tokens/{token_id}
Authorization: Bearer osk_{token}
```

The `token_id` is the first 16 hex chars of the SHA-256 hash of the full token -
safe to log, used as the public identifier in URLs. You can derive it locally
without hitting the server.

No API keys required for the spawn flow. No OAuth. No sessions. No cookies. No signup.

---

## Error handling (structured envelope, v1.1+)

Every non-2xx response returns the same shape:

```json
{
  "code":       "plan_not_found",
  "message":    "Unknown plan: foo.",
  "detail":     { "planId": "foo" },
  "request_id": "req_01JAXYZ...",
  "error":      "plan_not_found"
}
```

**Match on `code`** (stable, machine-readable). `message` is human-readable and
may change. `detail` is endpoint-specific diagnostic data. `request_id` correlates
to server logs - include it when asking for support. `error` is a legacy alias
of `code` kept for one release.

Every response also carries an `X-Request-Id` header.

### Canonical error codes

| `code` | HTTP | Meaning |
|---|---|---|
| `validation_failed` | 400 | Body/params don't pass validation |
| `invalid_idempotency_key` | 400 | Header fails 16-128 char / charset regex |
| `idempotency_key_reused` | 409 | Same key, different body |
| `plan_not_found` | 404 | Unknown planId |
| `order_not_found` | 404 | No such orderId |
| `request_not_found` | 404 | No such request_id (v1.3.0; may have expired) |
| `unauthorized` | 401 | Missing / malformed Bearer |
| `token_expired` | 401 | `expires_at` is in the past |
| `token_revoked` | 401 | `revoked_at` is set |
| `forbidden` | 403 | Valid token, wrong resource |
| `rate_limited` | 429 | Includes `Retry-After` header in seconds |
| `provisioning_failed` | 500 | Cloud-provider or cloud-init error (`detail.reason`) |
| `internal_error` | 500 | Anything unexpected |
| `service_unavailable` | 503 | x402 facilitator offline, or spawn provider unavailable |
| (payment-required envelope) | 402 | x402 - `{x402Version, error, accepts[]}`, not the Error shape |

---

## Rate limits

| Bucket | Limit | Always on |
|---|---|---|
| Per-IP, free endpoints (plans, status, docs, tokens, agent-card) | 30/min | yes |
| Per-IP, spawn | 5/min | yes |
| Per-token, spawn | 10/hour | when `Authorization: Bearer osk_...` is on spawn |
| Per-token, status | 120/min | on the Bearer-authenticated path |
| Per-token, WebSocket concurrent sessions | 3 | rejection pre-upgrade |

All `429` responses include a `Retry-After` header (seconds).

---

## WebSocket chat (`/api/v1/chat/{orderId}`)

- **Auth:** `?token=osk_...` query parameter. Validated pre-upgrade (`401` on
  miss/malformed, `401 token_expired` / `401 token_revoked` as appropriate,
  `403 forbidden` on wrong-order).
- **Max frame:** 64 KB.
- **Heartbeat:** server pings every 30 s; missed pong terminates.
- **Idle timeout:** `close 4003 idle_timeout` after 10 min of no client messages.
- **Concurrency:** max 3 sessions per token; 4th rejected with
  `429 + X-Auth-Reason: too_many_connections`.
- **Backpressure:** if your client falls behind (server-side
  `bufferedAmount > 1 MB`), server emits `{"type":"backpressure_pause"}` and
  stops forwarding until drained. Frames are dropped while paused, not queued.

**Client -> server frames:**

```json
{ "type": "chat",  "text": "Deploy a Python API on port 8080" }
{ "type": "abort" }
```

**Server -> client frames:**

```json
{ "type": "status",              "agent_connected": true }
{ "type": "text",                "text": "All systems ..." }
{ "type": "tool_use",            "name": "system_query" }
{ "type": "tool_result" }
{ "type": "done" }
{ "type": "error",               "code": "...", "text": "..." }
{ "type": "backpressure_pause"  }
{ "type": "backpressure_resume" }
```

For a higher-fidelity, replayable view of the same conversation, subscribe to
`/api/v1/servers/{orderId}/events` (v1.3.0) - chat messages emit
`request.accepted/progress/completed/failed` there too.

---

## What osmoda can do on a spawned server

The agent has **92 MCP tools** across 10 daemons. Grouped:

- **System:** `system_health`, `system_query`, `system_discover`, `event_log`, `journal_logs`, `network_info`, `service_status`
- **Files + shell:** `file_read`, `file_write`, `directory_list`, `shell_exec`
- **Deploy:** `safe_switch_begin` / `safe_switch_commit` / `safe_switch_rollback` (atomic deploys)
- **Apps:** `app_deploy`, `app_list`, `app_logs`, `app_stop`, `app_restart`, `app_remove`
- **Watchers + routines:** `watcher_add`, `watcher_list`, `routine_add`, `routine_list`, `routine_trigger`
- **Memory:** `memory_store`, `memory_recall` (vector + FTS5 BM25 hybrid)
- **Voice:** `voice_speak`, `voice_transcribe`, `voice_record`, `voice_listen`
- **Mesh:** `mesh_identity`, `mesh_invite_create`, `mesh_invite_accept`, `mesh_peers`, `mesh_peer_send`, mesh rooms
- **Crypto:** `wallet_create`, `wallet_list`, `wallet_sign`, `wallet_send`, `wallet_build_tx`, `wallet_delete`, `wallet_receipt`
- **Learning:** `teach_status`, `teach_patterns`, `teach_knowledge`, `teach_optimize_suggest/apply`, `teach_skill_*`
- **Safety:** `safety_rollback`, `safety_panic`, `safety_restart`
- **Backup:** `backup_create`, `backup_list`
- **MCP:** `mcp_servers`, `mcp_server_start/stop/restart`
- **Approval:** `approval_request`, `approval_pending`, `approval_approve`, `approval_check`
- **Sandbox:** `sandbox_exec`, `capability_mint`
- **Fleet:** `fleet_propose`, `fleet_status`, `fleet_vote`, `fleet_rollback`
- **Channels:** `message` (Telegram, WhatsApp routing)

Ask the agent anything. It will chain these tools to accomplish it.

---

## Per-server config API (dashboard-authed)

If you use the dashboard (session cookie), you can manage the server's agents,
credentials, and runtime engines without SSH. These endpoints proxy to the
customer server's `osmoda-gateway` over SSH:

```
# Runtime engines available on this server
GET    /api/dashboard/servers/{orderId}/config/drivers

# Agent profiles (osmoda, mobile, ...)
GET    /api/dashboard/servers/{orderId}/config/agents
PUT    /api/dashboard/servers/{orderId}/config/agents                 # replace all
PATCH  /api/dashboard/servers/{orderId}/config/agents/{agentId}       # partial update
DELETE /api/dashboard/servers/{orderId}/config/agents/{agentId}

# Credentials (encrypted at rest in credentials.json.enc)
GET    /api/dashboard/servers/{orderId}/config/credentials
POST   /api/dashboard/servers/{orderId}/config/credentials
POST   /api/dashboard/servers/{orderId}/config/credentials/{credId}/test
POST   /api/dashboard/servers/{orderId}/config/credentials/{credId}/default
DELETE /api/dashboard/servers/{orderId}/config/credentials/{credId}
```

**Valid runtimes (v1.2):** `claude-code`, `openclaw`
**Valid providers:** `anthropic`, `openai`, `openrouter`
**Valid credential types:** `oauth` (Anthropic subscription only), `api_key`
**Valid models (typical):** `claude-opus-4-7`, `claude-sonnet-4-6`, `claude-haiku-4-5-20251001`, `gpt-4o`, `gpt-5`, `o3-mini`

Saving an agent PATCH fires `SIGHUP` on the gateway. In-flight chat sessions
keep their original driver + credential snapshot; new sessions see the new config.
Zero WebSocket drops.

---

## What happens after spawn

1. The cloud provider creates a dedicated VM in your chosen region.
2. Cloud-init fetches `install.sh` from GitHub, converts to NixOS, builds the
   10 daemons, installs `osmoda-gateway` + `claude` CLI + the 92-tool MCP
   bridge (~5-10 minutes).
3. If you passed `credentials` / `runtime` / `default_model` at spawn time, they
   are written into `/var/lib/osmoda/config/agents.json` +
   `credentials.json.enc` at install time. The agent is ready on first boot.
4. Server heartbeats to `spawn.os.moda` every 5 minutes. Each heartbeat emits
   a `heartbeat.received` event on the v1.3.0 stream.
5. Status changes `provisioning` -> `running`. Once the customer box confirms
   credential delivery, `state.changed { field: 'has_api_key', value: true }`
   fires.
6. Agent becomes available via `/ws`, Telegram (if configured), or direct SSH.

The server runs autonomously. The agent monitors its own health, restarts
crashed services, deploys apps, edits NixOS config, runs `safe_switch` for
changes it can't revert trivially. Wedged-server detector (v1.2.7) auto-kicks
a restart on stale heartbeat; the restart emits `agent.wedged` then
`agent.healed` events.

---

## Mesh networking (agent-to-agent)

If you spawn multiple servers, they can form a direct encrypted mesh.
Noise_XX + X25519 + ML-KEM-768 (hybrid post-quantum). No central server.
Invite-based pairing, short-TTL codes.

Ask either agent: *"Create a mesh invite"* -> copy the code -> ask the other:
*"Accept this mesh invite: `<code>`"*. They're connected. Chat between them,
coordinate deploys, share context.

---

## Self-install (bring your own server)

If you already have a server and want osmoda without the spawn API:

```bash
curl -fsSL https://raw.githubusercontent.com/bolivian-peru/os-moda/main/scripts/install.sh | sudo bash

# Pre-configure the agent at install time:
curl -fsSL https://raw.githubusercontent.com/bolivian-peru/os-moda/main/scripts/install.sh | sudo bash -s -- \
  --runtime claude-code \
  --default-model claude-opus-4-7 \
  --credential "My Claude Pro|anthropic|oauth|$(printf 'sk-ant-oat01-...' | base64)"
```

Or with NixOS flakes:

```nix
inputs.os-moda.url = "github:bolivian-peru/os-moda";
# configuration.nix:
imports = [ os-moda.nixosModules.default ];
services.osmoda.enable = true;
```

Full docs: https://github.com/bolivian-peru/os-moda

---

## TypeScript SDK

`@osmoda/client` is a handwritten TypeScript SDK that mirrors this API:

```ts
import { OsmodaClient } from "@osmoda/client";

const client = new OsmodaClient({ bearer: "osk_..." });
const { plans } = await client.listPlans();
const server = await client.spawn("starter", {
  region: "eu-central",
  runtime: "claude-code",
  credentials: [{ label: "Claude Pro", provider: "anthropic", type: "oauth", secret: "sk-ant-oat01-..." }],
}, { idempotencyKey: "myapp-" + crypto.randomUUID() });
const ready = await client.waitForRunning(server.order_id);
```

Repo: https://github.com/bolivian-peru/os-moda/tree/main/packages/osmoda-client

---

## Trust model

```
Tier 0: osmoda's own agent          Root. Full system. This IS the OS interface.
Tier 1: Approved apps                Sandboxed; declared capabilities; egress proxy.
Tier 2: Untrusted tools              Max isolation, no network, minimal fs.
```

Every system mutation is logged to a SHA-256 hash-chained append-only SQLite
ledger (`agentctl verify-ledger`). Destructive operations require explicit user
approval via `approval_request` / `approval_approve`. NixOS atomic rollback is
available for every system change. The agent has root - but every action is
auditable and reversible.

Full security model: https://github.com/bolivian-peru/os-moda/blob/main/docs/SECURITY.md

---

## Quick reference

Spawn Solo with defaults:

```
POST https://spawn.os.moda/api/v1/spawn/test
Content-Type: application/json
Idempotency-Key: <your-key>
(x402 USDC payment required - $14.99 on Base Sepolia or Solana Devnet)
{}
```

Spawn Pro in us-east, bring your Claude Pro OAuth for the agent:

```
POST https://spawn.os.moda/api/v1/spawn/starter
Content-Type: application/json
Idempotency-Key: <your-key>
{
  "region": "us-east",
  "runtime": "claude-code",
  "credentials": [{
    "label": "My Claude Pro", "provider": "anthropic", "type": "oauth",
    "secret": "sk-ant-oat01-..."
  }]
}
```

Check ready:

```
GET https://spawn.os.moda/api/v1/status/{orderId}
Authorization: Bearer osk_{token}
```

Talk to the agent (WebSocket):

```
wss://spawn.os.moda/api/v1/chat/{orderId}?token=osk_{token}
Send:    { "type": "chat", "text": "your instruction here" }
Receive: { "type": "text" | "tool_use" | "tool_result" | "done" | ... }
```

Watch everything happening server-side (v1.3.0 SSE):

```
GET /api/v1/servers/{orderId}/events?cursor=0&filter=request,state,agent
Authorization: Bearer osk_{token}
Accept: text/event-stream
```

Revoke your token when done:

```
DELETE https://spawn.os.moda/api/v1/tokens/{token_id}
Authorization: Bearer osk_{token}
```

Pay. Get server. Talk to its AI. Watch the events. Revoke when done.
