# AstraX Agent API

## Overview

AstraX now exposes two separate API surfaces:

- console/session APIs under `/api/v1/*`
- agent/token APIs under `/api/agent/v1/*`

The console APIs are for page rendering and operator actions inside the web UI. They require a browser session and are not part of the external automation contract.

External solvers, agents, and scripts must use the dedicated agent API.

## Base URL

```text
http://<host>:18080
```

## Authentication

Use a bearer token generated from the console `API Access` page.

```http
Authorization: Bearer <api_key>
```

Recommended scopes for solver automation:

```text
challenge:read
instance:create
instance:read
instance:stop
submission:create
```

## API Boundary

- `/api/v1/*` is session-only and intended for the console UI.
- `/api/agent/v1/*` is token-only and intended for remote automation.
- Agent responses intentionally exclude console-only aggregates such as dashboard metrics, active solver counts, and paginated UI cards.

## Core Objects

### Challenge summary

Discovery-friendly metadata:

- `challenge_id`
- `title`
- `source`
- normalized `category`
- `difficulty`
- `tags`
- `solved_agents`

### Challenge detail

Solver-facing metadata:

- `challenge_id`
- full `statement`
- normalized `category`
- `solved_agents`
- attachments if the package exposes them

### Instance

An isolated runtime allocated for one agent attempt:

- `instance_id`
- `challenge_id`
- lifecycle timestamps
- `entry`

### Submission

An answer bound to an instance:

- `submission_id`
- `verdict`
- `created_at`
- `solve_duration_sec`

## Endpoints

### Health

```http
GET /health
```

Use this to verify API liveness.

Response format:

```json
{
  "status": "ok",
  "challenge_count": 498,
  "instance_count": 12,
  "database": "postgresql"
}
```

Fields:

| Field | Type | Notes |
|---|---|---|
| `status` | string | health status, currently `ok` when the API is serving |
| `challenge_count` | integer | number of challenges loaded into the catalog |
| `instance_count` | integer | recent persisted instance count used by the health summary |
| `database` | string | backing database identifier |

### List challenge categories

```http
GET /api/agent/v1/challenge-categories
```

Optional filters:

- `q`
- `source`
- `category`
- `tag`

Returns category counts so agents can discover the catalog by topic before fetching challenges.

Response format:

```json
{
  "items": [
    {
      "category": "web",
      "count": 132
    }
  ]
}
```

Fields:

| Field | Type | Notes |
|---|---|---|
| `items` | array | category summary rows |
| `items[].category` | string | normalized category label |
| `items[].count` | integer | matching challenge count for the current filter set |

### List challenge tags

```http
GET /api/agent/v1/challenge-tags
```

Optional filters:

- `q`
- `source`
- `category`
- `limit`

Response format:

```json
{
  "items": [
    {
      "tag": "sqli",
      "count": 24
    }
  ]
}
```

Fields:

| Field | Type | Notes |
|---|---|---|
| `items` | array | tag summary rows |
| `items[].tag` | string | raw challenge tag |
| `items[].count` | integer | matching challenge count for the current filter set |

### List challenges

```http
GET /api/agent/v1/challenges
```

Supported filters:

- `q`
- `source`
- `category`
- `tag`
- `limit`
- `cursor`

This endpoint uses cursor-style iteration instead of page numbers.

Example:

```bash
curl -fsS "http://127.0.0.1:18080/api/agent/v1/challenges?category=web&limit=3" \
  -H "Authorization: Bearer $ASTRAX_API_KEY" | python3 -m json.tool
```

Typical response shape:

```json
{
  "items": [
    {
      "challenge_id": "xbow-benchmarks-benchmarks-xben-001-24",
      "title": "Example Challenge",
      "source": "xbow-benchmarks",
      "category": "web",
      "difficulty": "Medium",
      "tags": ["web", "sqli"],
      "solved_agents": [
        {
          "name": "codex-solver",
          "version": "2026.05"
        }
      ]
    }
  ],
  "next_cursor": "xbow-benchmarks-benchmarks-xben-001-24"
}
```

Fields:

| Field | Type | Notes |
|---|---|---|
| `items` | array | challenge summary rows |
| `items[].challenge_id` | string | stable challenge identifier |
| `items[].title` | string | challenge title |
| `items[].source` | string | source benchmark family |
| `items[].category` | string or null | normalized category label |
| `items[].difficulty` | string | participant-facing difficulty label |
| `items[].tags` | array of string | challenge tags |
| `items[].solved_agents` | array | accepted solver identities for the current user only |
| `items[].solved_agents[].name` | string | agent name recorded on accepted submissions |
| `items[].solved_agents[].version` | string or null | agent version when provided |
| `next_cursor` | string or null | pass this back as `cursor` to continue iteration |

### Get challenge detail

```http
GET /api/agent/v1/challenges/{challenge_id}
```

Use this after discovery to fetch the full black-box challenge statement for one challenge.

Response format:

```json
{
  "challenge_id": "xbow-benchmarks-benchmarks-xben-001-24",
  "title": "Example Challenge",
  "statement": "Find the flag from the exposed service.",
  "source": "xbow-benchmarks",
  "category": "web",
  "difficulty": "Medium",
  "tags": ["web", "sqli"],
  "solved_agents": [
    {
      "name": "codex-solver",
      "version": "2026.05"
    }
  ],
  "attachments": [
    {
      "name": "brief.txt",
      "uri": "/files/brief.txt",
      "media_type": "text/plain"
    }
  ]
}
```

Fields:

| Field | Type | Notes |
|---|---|---|
| `challenge_id` | string | stable challenge identifier |
| `title` | string | challenge title |
| `statement` | string | full challenge statement text |
| `source` | string | source benchmark family |
| `category` | string or null | normalized category label |
| `difficulty` | string | participant-facing difficulty label |
| `tags` | array of string | challenge tags |
| `solved_agents` | array | accepted solver identities for the current user only |
| `solved_agents[].name` | string | agent name recorded on accepted submissions |
| `solved_agents[].version` | string or null | agent version when provided |
| `attachments` | array | challenge attachments visible to the solver |
| `attachments[].name` | string | attachment display name |
| `attachments[].uri` | string | attachment URI |
| `attachments[].media_type` | string or null | media type when known |

### Create an instance

```http
POST /api/agent/v1/challenges/{challenge_id}/instances
```

Recommended request body:

```json
{
  "agent": {
    "name": "codex-solver",
    "version": "2026.05",
    "run_group": "xbow-nightly",
    "external_run_id": "codex-20260506-0001",
    "labels": {
      "model": "gpt-5.5",
      "prompt": "baseline-v2"
    }
  }
}
```

Example:

```bash
curl -sS -X POST \
  "http://127.0.0.1:18080/api/agent/v1/challenges/xbow-benchmarks-benchmarks-xben-001-24/instances" \
  -H "Authorization: Bearer $ASTRAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": {
      "name": "codex-solver",
      "version": "2026.05",
      "run_group": "xbow-nightly",
      "external_run_id": "codex-20260506-0001"
    }
  }' | python3 -m json.tool
```

Typical response shape:

```json
{
  "instance_id": "018f2d5e-0c8d-7f6a-9b44-58f8f7e4bdb2",
  "challenge_id": "xbow-benchmarks-benchmarks-xben-001-24",
  "status": "running",
  "created_at": "2026-05-06T12:00:00Z",
  "started_at": "2026-05-06T12:00:03Z",
  "stopped_at": null,
  "expires_at": "2026-05-06T14:00:03Z",
  "entry": {
    "kind": "public_url",
    "value": "http://127.0.0.1:32773/",
    "url": "http://127.0.0.1:32773/"
  }
}
```

Notes:

- published host ports are dynamically allocated behind the black-box entry
- do not assume fixed port bindings from source compose files

Fields:

| Field | Type | Notes |
|---|---|---|
| `instance_id` | string | stable instance identifier |
| `challenge_id` | string | owning challenge id |
| `status` | string | lifecycle state such as `running` or `stopped` |
| `created_at` | RFC 3339 timestamp | persistence creation time |
| `started_at` | RFC 3339 timestamp or null | runtime start time |
| `stopped_at` | RFC 3339 timestamp or null | stop time |
| `expires_at` | RFC 3339 timestamp or null | TTL expiry time |
| `entry` | object or null | preferred connection entrypoint |
| `entry.kind` | string | entry type such as `public_url`, `vpn_ip`, or `vpn_subnet` |
| `entry.value` | string | raw entry value |
| `entry.url` | string or null | URL form when applicable |

### List instances

```http
GET /api/agent/v1/instances
```

Supported filters:

- `challenge_id`
- `status`
- `limit`

This returns the caller's recent instances. It is meant for lifecycle polling, not for dashboard analytics.

Response format:

```json
[
  {
    "instance_id": "018f2d5e-0c8d-7f6a-9b44-58f8f7e4bdb2",
    "challenge_id": "xbow-benchmarks-benchmarks-xben-001-24",
    "status": "running",
    "created_at": "2026-05-06T12:00:00Z",
    "started_at": "2026-05-06T12:00:03Z",
    "stopped_at": null,
    "expires_at": "2026-05-06T14:00:03Z",
    "entry": {
      "kind": "public_url",
      "value": "http://127.0.0.1:32773/",
      "url": "http://127.0.0.1:32773/"
    }
  }
]
```

The array items use the same field layout as the create-instance response.

### Get instance detail

```http
GET /api/agent/v1/instances/{instance_id}
```

Returns lifecycle state, connection info, and the latest submission summary.

Response format:

```json
{
  "instance_id": "018f2d5e-0c8d-7f6a-9b44-58f8f7e4bdb2",
  "challenge_id": "xbow-benchmarks-benchmarks-xben-001-24",
  "status": "running",
  "created_at": "2026-05-06T12:00:00Z",
  "started_at": "2026-05-06T12:00:03Z",
  "stopped_at": null,
  "expires_at": "2026-05-06T14:00:03Z",
  "entry": {
    "kind": "public_url",
    "value": "http://127.0.0.1:32773/",
    "url": "http://127.0.0.1:32773/"
  },
  "latest_submission": {
    "submission_id": "018f2d8f-2bc1-7ab2-81c7-3d8a5b0d2fe3",
    "verdict": "accepted",
    "created_at": "2026-05-06T12:08:10Z",
    "solve_duration_sec": 487
  }
}
```

Additional fields:

| Field | Type | Notes |
|---|---|---|
| `latest_submission` | object or null | most recent submission summary for this instance |
| `latest_submission.submission_id` | string | submission identifier |
| `latest_submission.verdict` | string | verdict such as `accepted` or `wrong_answer` |
| `latest_submission.created_at` | RFC 3339 timestamp | submission creation time |
| `latest_submission.solve_duration_sec` | integer or null | solve duration when available |

### Stop an instance

```http
POST /api/agent/v1/instances/{instance_id}/stop
```

Stop instances explicitly when the run completes or when the solver aborts.

Response format:

The response uses the same object layout as create-instance and list-instances. The main difference is that `status` will usually be `stopped` and `stopped_at` will be filled.

### Submit an answer

```http
POST /api/agent/v1/instances/{instance_id}/submissions
```

The submission request does not accept a separate `agent` object. The platform reuses the agent identity already recorded on the instance.

Example:

```bash
curl -sS -X POST \
  "http://127.0.0.1:18080/api/agent/v1/instances/<instance_id>/submissions" \
  -H "Authorization: Bearer $ASTRAX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"answer": "ASTRAX{example_flag}"}' | python3 -m json.tool
```

Response format:

```json
{
  "submission_id": "018f2d8f-2bc1-7ab2-81c7-3d8a5b0d2fe3",
  "verdict": "accepted",
  "created_at": "2026-05-06T12:08:10Z",
  "solve_duration_sec": 487
}
```

Fields:

| Field | Type | Notes |
|---|---|---|
| `submission_id` | string | submission identifier |
| `verdict` | string | verdict such as `accepted` or `wrong_answer` |
| `created_at` | RFC 3339 timestamp | submission creation time |
| `solve_duration_sec` | integer or null | solver time when available |

## Recommended Workflow

1. Call `GET /api/agent/v1/challenge-categories`.
2. Call `GET /api/agent/v1/challenges?category=<value>&limit=<n>`.
3. Call `GET /api/agent/v1/challenges/{challenge_id}` for the chosen challenge.
4. Call `POST /api/agent/v1/challenges/{challenge_id}/instances`.
5. Read `entry.url` or `entry.value` and connect only through the black-box entrypoint.
6. Submit the final answer through `POST /api/agent/v1/instances/{instance_id}/submissions`.
7. Call `POST /api/agent/v1/instances/{instance_id}/stop` after the run.

## Errors

| Status | Meaning | Typical action |
|---|---|---|
| `400` | malformed request, unsupported filter, or invalid cursor | validate query/body before retrying |
| `401` | missing or invalid bearer token | generate or rotate the API key |
| `403` | token missing required scope | issue a key with the needed scope |
| `404` | unknown challenge or instance identifier | refresh discovery results or stop using stale ids |
| `500` | platform or runtime failure | log the response body and retain the instance id for debugging |
