# Didactyl Admin HTTP API

## Overview

Didactyl exposes a localhost-only HTTP API for external tools and dashboards to inspect agent state, explore LLM context, craft prompts, and compare prompt variants. The API runs inside the same process as the agent — no separate server, no authentication required.

All responses are JSON. CORS headers are included on every response for browser access from any local origin.

---

## Configuration

Enable the API in `genesis.jsonc`:

```json
{
  "api": {
    "enabled": true,
    "port": 8484,
    "bind_address": "127.0.0.1"
  }
}
```

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | bool | `false` | Must be explicitly set to `true` to start the HTTP server |
| `port` | int | `8484` | TCP port to listen on |
| `bind_address` | string | `"127.0.0.1"` | Bind address — use `127.0.0.1` for localhost-only access |

The API is disabled by default. When disabled, no listener is created and no resources are consumed.

---

## CORS

Every response includes:

```
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type
```

`OPTIONS` requests return `204 No Content` with these headers for browser preflight support.

---

## Endpoints

### GET /api/status

Returns agent runtime status.

**Response:**

```json
{
  "success": true,
  "name": "Didactyl",
  "version": "v0.0.26",
  "pubkey": "52a3e82f7b3743852fbe804cfcbf4db3448115887895247c001f2b50e790acb8",
  "relay_count": 4,
  "connected_relays": 4,
  "active_triggers": 0
}
```

| Field | Description |
|---|---|
| `name` | Agent display name constant |
| `version` | Build version string |
| `pubkey` | Agent public key in hex |
| `relay_count` | Number of configured relays |
| `connected_relays` | Number of currently connected relays |
| `active_triggers` | Number of active triggered-skill subscriptions |

---

### GET /api/context/current

Returns the full LLM context message array that would be sent to the model right now. This is the same context the agent builds for an admin DM conversation.

**Response:**

```json
{
  "success": true,
  "total_chars": 13131,
  "total_estimated_tokens": 3283,
  "messages": [
    {"role": "system", "content": "# Didactyl Agent\n\nYou are Didactyl..."},
    {"role": "system", "content": "This is your administrator! Admin pubkey..."},
    {"role": "system", "content": "Administrator kind 0 profile content..."},
    {"role": "system", "content": "Administrator kind 10002 relay-list content..."},
    {"role": "system", "content": "Startup events memory..."},
    {"role": "system", "content": "Adopted skills memory..."},
    {"role": "system", "content": "Administrator recent public notes..."}
  ]
}
```

| Field | Description |
|---|---|
| `total_chars` | Total character count across all messages |
| `total_estimated_tokens` | Rough token estimate using `chars / 4` heuristic |
| `messages` | OpenAI-format messages array with role and content |

---

### GET /api/context/parts

Returns the context broken into labeled, individually-sized parts. Useful for understanding what consumes context budget.

**Response:**

```json
{
  "success": true,
  "total_chars": 13131,
  "total_estimated_tokens": 3283,
  "parts": [
    {
      "name": "system_prompt",
      "role": "system",
      "chars": 1200,
      "estimated_tokens": 300,
      "content": "# Didactyl Agent..."
    },
    {
      "name": "admin_identity",
      "role": "system",
      "chars": 120,
      "estimated_tokens": 30,
      "content": "This is your administrator!..."
    },
    {
      "name": "admin_kind0",
      "role": "system",
      "chars": 450,
      "estimated_tokens": 113,
      "content": "Administrator kind 0 profile content..."
    },
    {
      "name": "admin_relay_list",
      "role": "system",
      "chars": 50,
      "estimated_tokens": 13,
      "content": "Administrator kind 10002 relay-list content..."
    },
    {
      "name": "startup_events",
      "role": "system",
      "chars": 4800,
      "estimated_tokens": 1200,
      "content": "Startup events memory..."
    },
    {
      "name": "adopted_skills",
      "role": "system",
      "chars": 2100,
      "estimated_tokens": 525,
      "content": "Adopted skills memory..."
    },
    {
      "name": "admin_notes",
      "role": "system",
      "chars": 680,
      "estimated_tokens": 170,
      "content": "Administrator recent public notes..."
    }
  ],
  "messages": [...]
}
```

**Part names:**

| Name | Description |
|---|---|
| `system_prompt` | The agent base/default skill prompt (first system message) |
| `admin_identity` | Admin pubkey identification message |
| `admin_kind0` | Admin kind 0 profile metadata |
| `admin_relay_list` | Admin kind 10002 relay list |
| `startup_events` | Startup events memory block |
| `adopted_skills` | Adopted skills behavioral instructions |
| `admin_notes` | Admin recent kind 1 public notes |
| `dm_history` | Recent DM conversation history |
| `context_part` | Any other context message |

---

### POST /api/trigger/:d_tag

Fires a webhook-triggered skill by `d_tag`.

**Path params:**

| Param | Required | Description |
|---|---|---|
| `d_tag` | yes | Skill `d` tag of a skill configured with `trigger=webhook` |

**Request body:**

Optional JSON payload; if present it is passed into the synthetic trigger event as `payload`.

```json
{
  "source": "external-cron",
  "note": "run maintenance sweep"
}
```

**Response:**

```json
{
  "success": true,
  "d_tag": "maintenance-sweep",
  "fired": true
}
```

**Notes:**

- Returns `404` if no skill exists for `d_tag` or no active trigger is registered.
- Returns `400` if the matched skill is not a `webhook` trigger or is disabled.
- Trigger execution uses the same trigger cooldown policy as other trigger types.

---

### POST /api/prompt/run-simple

Submit a system prompt and user message for a simple LLM call with no tools. Useful for quick prompt iteration.

**Request:**

```json
{
  "system": "You are a helpful assistant that writes tweets.",
  "user": "Write a tweet about AI agents on Nostr",
  "model": "claude-haiku-4.5"
}
```

| Field | Required | Description |
|---|---|---|
| `system` | yes | System prompt string |
| `user` | yes | User message string |
| `model` | no | Override the current model for this request only |

**Response:**

```json
{
  "success": true,
  "response": "AI agents are finding their home on Nostr...",
  "model_used": "claude-haiku-4.5",
  "input_tokens_estimate": 85,
  "output_tokens_estimate": 42
}
```

---

### POST /api/prompt/agent

Submit one user message and let Didactyl build full admin context server-side (same context assembly path used for Nostr admin DMs, including admin profile, adopted skills, and recent DM history).

**Request:**

```json
{
  "message": "Tweet about the weather",
  "model": "claude-haiku-4.5",
  "max_turns": 5
}
```

| Field | Required | Description |
|---|---|---|
| `message` | yes | User message string |
| `model` | no | Override the current model for this request only |
| `max_turns` | no | Maximum tool-call loop iterations (default: 4, max: 16) |

**Response:**

```json
{
  "success": true,
  "final_response": "Done! I posted a tweet about the weather.",
  "turns": [
    {
      "turn": 1,
      "tool_calls": [
        {
          "name": "nostr_post",
          "arguments": "{\"kind\":1,\"content\":\"Beautiful day!\"}",
          "result": "{\"success\":true,\"event_id\":\"abc123\"}"
        }
      ]
    }
  ],
  "model_used": "claude-haiku-4.5",
  "total_input_tokens_estimate": 3200,
  "total_output_tokens_estimate": 180
}
```

| Field | Description |
|---|---|
| `final_response` | The LLM final text response after all tool calls complete |
| `turns` | Array of turn objects, each containing tool calls made in that turn |
| `turns[].tool_calls[]` | Each tool call with name, arguments JSON, and result JSON |
| `model_used` | The model that was actually used |
| `total_input_tokens_estimate` | Estimated input tokens for the full conversation |
| `total_output_tokens_estimate` | Estimated output tokens for the final response |

---

### POST /api/prompt/run

Submit a full messages array with the agent tool set enabled. This endpoint runs exactly what you provide and does **not** auto-build Didactyl admin context.

**Request:**

```json
{
  "messages": [
    {"role": "system", "content": "You are Didactyl..."},
    {"role": "user", "content": "Tweet about the weather"}
  ],
  "model": "claude-haiku-4.5",
  "max_turns": 5
}
```

| Field | Required | Description |
|---|---|---|
| `messages` | yes | OpenAI-format messages array |
| `model` | no | Override the current model for this request only |
| `max_turns` | no | Maximum tool-call loop iterations (default: 4, max: 16) |

**Response:**

```json
{
  "success": true,
  "final_response": "Done! I posted a tweet about the weather.",
  "turns": [
    {
      "turn": 1,
      "tool_calls": [
        {
          "name": "nostr_post",
          "arguments": "{\"kind\":1,\"content\":\"Beautiful day!\"}",
          "result": "{\"success\":true,\"event_id\":\"abc123\"}"
        }
      ]
    }
  ],
  "model_used": "claude-haiku-4.5",
  "total_input_tokens_estimate": 3200,
  "total_output_tokens_estimate": 180
}
```

| Field | Description |
|---|---|
| `final_response` | The LLM final text response after all tool calls complete |
| `turns` | Array of turn objects, each containing tool calls made in that turn |
| `turns[].tool_calls[]` | Each tool call with name, arguments JSON, and result JSON |
| `model_used` | The model that was actually used |
| `total_input_tokens_estimate` | Estimated input tokens for the full conversation |
| `total_output_tokens_estimate` | Estimated output tokens for the final response |

---

### POST /api/prompt/compare

A/B testing: submit two prompt variants, both are executed sequentially, responses returned side-by-side. Each variant can optionally use a different model for cross-model comparison.

**Request:**

```json
{
  "variant_a": {
    "messages": [
      {"role": "system", "content": "You are concise. No emoji."},
      {"role": "user", "content": "Say hi."}
    ],
    "model": "claude-haiku-4.5",
    "max_turns": 1
  },
  "variant_b": {
    "messages": [
      {"role": "system", "content": "You are verbose and friendly."},
      {"role": "user", "content": "Say hi."}
    ],
    "model": "claude-haiku-4.5",
    "max_turns": 1
  }
}
```

| Field | Required | Description |
|---|---|---|
| `variant_a` | yes | First prompt variant — same shape as `/api/prompt/run` request |
| `variant_b` | yes | Second prompt variant — same shape as `/api/prompt/run` request |

**Response:**

```json
{
  "success": true,
  "variant_a": {
    "success": true,
    "final_response": "Hi. How can I help?",
    "turns": [{"turn": 1, "tool_calls": []}],
    "model_used": "claude-haiku-4.5",
    "total_input_tokens_estimate": 21,
    "total_output_tokens_estimate": 6
  },
  "variant_b": {
    "success": true,
    "final_response": "Hey there! Nice to meet you! I am here and ready to help...",
    "turns": [{"turn": 1, "tool_calls": []}],
    "model_used": "claude-haiku-4.5",
    "total_input_tokens_estimate": 21,
    "total_output_tokens_estimate": 72
  }
}
```

Variant A runs first, then variant B. The original model config is restored after each variant completes.

---

## Error Responses

All error responses follow this shape:

```json
{
  "success": false,
  "error": "description of what went wrong"
}
```

Common HTTP status codes:

| Code | Meaning |
|---|---|
| `200` | Success |
| `204` | OPTIONS preflight success |
| `400` | Bad request — missing or invalid parameters |
| `404` | Endpoint not found |
| `500` | Internal server error |

---

## Token Estimation

All token estimates use a simple `chars / 4` heuristic. This is a rough approximation that works well enough for English text across major model families. No real tokenizer is used.

---

## Security

- Binds to `127.0.0.1` only — not accessible from the network
- No authentication — this is a local development and administration tool
- The `api.enabled` config flag defaults to `false` and must be explicitly opted in
- Prompt execution endpoints have full tool access equivalent to admin-tier DM conversations
- The agent process must be running for the API to be available

---

## Architecture

The HTTP server is embedded in the didactyl process using the Mongoose library. It runs in the same thread as the main poll loop — each iteration calls `http_api_poll()` which does non-blocking accept/read/write. This avoids threading complexity and gives the API direct access to all agent state.

```mermaid
flowchart LR
    subgraph didactyl process
        MAIN[main loop] --> POLL[nostr_handler_poll]
        MAIN --> TPOLL[trigger_manager_poll]
        MAIN --> HPOLL[http_api_poll]
        HPOLL --> ROUTER[request router]
        ROUTER --> AGENT[agent internals]
        ROUTER --> LLM[LLM client]
        ROUTER --> TOOLS[tools context]
        ROUTER --> CONFIG[config]
        ROUTER --> TRIGGERS[trigger_manager]
    end
    BROWSER[Web Dashboard] -- HTTP localhost:8484 --> HPOLL
```

### Source Files

| File | Purpose |
|---|---|
| `src/http_api.c` | HTTP server, request router, all endpoint handlers |
| `src/http_api.h` | Public API: `http_api_init`, `http_api_poll`, `http_api_cleanup` |
| `src/mongoose.c` | Mongoose embedded HTTP library |
| `src/mongoose.h` | Mongoose header |

---

## Future Endpoints

The following endpoints are planned but not yet implemented:

| Method | Path | Description |
|---|---|---|
| GET | `/api/config` | Current runtime config with redacted secrets |
| GET | `/api/events/skills` | List published skills |
| GET | `/api/events/skills/:d_tag` | Fetch skill by d_tag |
| PUT | `/api/events/skills/:d_tag` | Update skill |
| GET | `/api/events/adoption` | Fetch adoption list |
| GET | `/api/events/profile` | Fetch agent profile |
| PUT | `/api/events/profile` | Update agent profile |
| GET | `/api/triggers` | List active triggers |
| GET | `/api/model` | Current model config |
| PUT | `/api/model` | Update model config |
| GET | `/api/models` | List available models |
| GET | `/api/relays` | Relay connection status |
| GET | `/api/tools` | List tool schemas |
| POST | `/api/tools/:name/execute` | Execute a tool directly |
| GET | `/api/context/log` | Recent context.log entries |
| POST | `/api/context/preview` | Dry-run context preview |
