> ## Documentation Index
> Fetch the complete documentation index at: https://dev-doc.dualmindlab.tech/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These are the DualMind developer and admin docs. The API base URL is https://api.dualmindlab.tech and the local API runs on http://localhost:5079.
> Never send POST, PUT, PATCH or DELETE requests to the production API while developing; use a local API and a development Supabase project.
> Environment variables are documented by name only. Never ask for or print secret values.
> Admin routes under /api/admin require a Supabase access token for a user whose public.users.role is admin. The PUT routes for users, models, providers and threads write every field, so send the full record; PUT /api/admin/keys/{keyId} writes only the fields you send (sending api_key replaces the key).

# Admin API

> The /api/admin routes behind the admin panel: access, paging, the response envelope and the behaviours to know before you call them.

<Panel>
  <Card title="At a glance" icon="shield-check">
    * **Base:** `https://api.dualmindlab.tech/api/admin`
    * **Auth:** Bearer token, `role = 'admin'`
    * **Routes:** 46 across 9 resources
    * **Paging:** `page`, `pageSize` (max 500)
    * **Envelope:** `success`, `data`, `total`, `error`
  </Card>

  <RequestExample>
    ```bash Check admin access theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    curl https://api.dualmindlab.tech/api/admin/dashboard/health \
      -H "Authorization: Bearer $ADMIN_TOKEN"
    ```
  </RequestExample>

  <ResponseExample>
    ```json 200 theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    {
      "success": true,
      "data": { "status": "healthy", "supabase_connected": true }
    }
    ```

    ```text 403 theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    Signed in, but public.users.role is not admin.
    ```
  </ResponseExample>
</Panel>

Nine controllers under `/api/admin` expose admin operations for users, models, providers, keys, comparisons, votes, threads and messages, plus dashboard metrics. Each one carries `[Authorize(Policy = "AdminOnly")]`. `model_leaderboard`, `system_settings` and `telegram_sessions` have no admin routes; change them in Supabase.

## Access

<Warning>
  **Production is read-only while you develop.** Point writes (battles, votes, admin changes) at `http://localhost:5079` and a development Supabase project. Against `https://api.dualmindlab.tech`, stick to `GET`s on public endpoints.
</Warning>

```http theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
Authorization: Bearer <access token of a user with role = 'admin'>
```

* `401`: no token, or the token failed validation.
* `403`: valid token, but `public.users.role` isn't `admin`.
* `GET /api/admin/dashboard/health` is the cheap way to check both. The admin panel calls it right after sign-in.

## Resources

<CardGroup cols={3}>
  <Card title="Dashboard" icon="layout-dashboard" href="/api-reference/admin/dashboard/get-the-dashboard">Metrics, provider health, recent activity</Card>
  <Card title="Users" icon="users" href="/api-reference/admin/users/list-users">Users and roles</Card>
  <Card title="AI models" icon="cpu" href="/api-reference/admin/models/list-models">Catalog and status</Card>
  <Card title="Providers" icon="plug" href="/api-reference/admin/providers/list-providers">Provider registry</Card>
  <Card title="API keys" icon="key-round" href="/api-reference/admin/keys/list-provider-keys">Key pool, toggles, cooldowns</Card>
  <Card title="Comparisons" icon="columns-2" href="/api-reference/admin/comparisons/list-comparisons">Battles</Card>
  <Card title="Votes" icon="vote" href="/api-reference/admin/votes/list-votes">Votes and statistics</Card>
  <Card title="Threads" icon="messages-square" href="/api-reference/admin/threads/list-threads">Threads and visibility</Card>
  <Card title="Messages" icon="message-square-text" href="/api-reference/admin/messages/list-thread-messages">Thread messages</Card>
</CardGroup>

## Lists and paging

Paginated routes (`/users`, `/models`, `/keys`, `/comparisons`, `/votes`, `/threads` and `/messages`) take `page` (from 1) and `pageSize` (1 to 500, default 50) and return the matching row count as `total`. `/providers` and `/models/active` are unpaged and return `total` only; active models are sorted by `model_name`.

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{ "success": true, "data": [ … ], "total": 128, "page": 2, "pageSize": 50, "page_size": 50 }
```

Paginated results are newest first. `search` and the filters (`role`, `status`, `provider`, `visibility`, `userId`, `modelId`, `comparisonId`, `threadId`, `isRevealed`) combine with AND.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
  curl "https://api.dualmindlab.tech/api/admin/users?page=1&pageSize=25&role=admin" \
    -H "Authorization: Bearer $ADMIN_TOKEN"
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
  const res = await fetch(
    "https://api.dualmindlab.tech/api/admin/users?page=1&pageSize=25&role=admin",
    { headers: { Authorization: `Bearer ${adminToken}` } },
  );
  const { data, total } = await res.json();
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
  import requests

  res = requests.get(
      "https://api.dualmindlab.tech/api/admin/users",
      params={"page": 1, "pageSize": 25, "role": "admin"},
      headers={"Authorization": f"Bearer {admin_token}"},
      timeout=30,
  )
  body = res.json()
  print(body["total"], [u["email"] for u in body["data"]])
  ```
</CodeGroup>

## Behaviours to know

<AccordionGroup>
  <Accordion title="PUT writes every field" icon="file-pen">
    `PUT /users/{id}`, `/models/{id}`, `/providers/{name}` and `/threads/{id}` serialize the whole request DTO, so any field you leave out is stored as `null`. Read the record, change what you need, send it all back. Use the `PATCH` routes to change a single field (role, status, visibility, key state). `PUT /keys/{keyId}` is the exception: it only writes the fields you send.
  </Accordion>

  <Accordion title="IDs, not names" icon="hash">
    Comparisons and votes return `model1_id`, `model2_id`, `winner_model_id` and `user_id`. Resolve names with `GET /models` and `GET /users`.
  </Accordion>

  <Accordion title="Database errors pass through" icon="database">
    When PostgREST rejects a write (a foreign key, a duplicate) the route returns `400` with the PostgREST JSON as a string in `error`. Parse it if you want the `message` field.
  </Accordion>

  <Accordion title="Provider and key changes apply immediately" icon="zap">
    Every write to `/providers` and `/keys` refreshes the API's in-memory provider cache. Model changes don't touch the model cache, which refreshes within five minutes.
  </Accordion>

  <Accordion title="Users: rows, not accounts" icon="user">
    `POST /users` and `DELETE /users/{id}` work on `public.users` only. Supabase Auth accounts are created by signing up and removed in Supabase.
  </Accordion>

  <Accordion title="Raw inserts for votes, threads and messages" icon="flask-conical">
    `POST /votes`, `/threads` and `/messages` insert rows directly, without the arena's checks. Use them for repairs and test data in non-production environments; the admin panel doesn't expose them.
  </Accordion>

  <Accordion title="Ignore health.redis" icon="circle-off">
    The dashboard's `health.redis` is always `offline`; there is no Redis.
  </Accordion>
</AccordionGroup>

## Outside this API

The admin panel's Email page calls the Supabase Edge Function `send-admin-email` (`{ emails, subject, title, html }` → `{ sent, failed }`), not this API. Feature flags can be read with `GET /api/settings/feature-flag/{key}` but are changed in the `system_settings` table.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.