# Euphemist Moderation API

A review-moderation API: send it a sentence, get back whether it's offensive
and, if so, six rewritten alternatives that keep the original meaning
without the harmful language.

Runs a fallback chain across Groq and Gemini models, so a single provider or
model outage doesn't take the API down. If every provider is unreachable or
unconfigured, requests still succeed — you get a locally-masked alternative
instead of an error.

## Base URL

```
http://<your-host>:<port>/api/v1
```

## Authentication

Every request (except `/health`) requires an API key in the `x-api-key`
header:

```
x-api-key: eupm_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Ask the API operator for a key, or if you run this service yourself, issue
one with:

```bash
npm run create-key -- "Some Developer or App Name"

# or, if you run it in Docker:
docker compose exec app node scripts/create-key.js "Some Developer or App Name"
```

The key is printed once — store it securely. Requests without a valid key
get `401`.

## Rate limits

30 requests per minute per API key. Exceeding it returns `429`.

## Endpoints

### `GET /health`

No API key required. Use this to check the service is reachable and has at
least one LLM provider configured.

```bash
curl http://localhost:3100/api/v1/health
```

```json
{ "status": "ok", "providersConfigured": true }
```

### `GET /tasks`

No API key required. Lists the tasks the universal `/process` endpoint
currently supports.

```bash
curl http://localhost:3100/api/v1/tasks
```

```json
{
  "tasks": [
    { "name": "moderate", "description": "Classify a single review sentence; returns rewritten alternatives if offensive." },
    { "name": "moderate_more", "description": "Generate 3 additional rewrite alternatives distinct from ones already shown." }
  ]
}
```

### `POST /process` (recommended)

One universal endpoint for every task above and any task added in the
future. Request and response always share the same shape:

```
{ "task": "<task name>", "input": { ...task-specific fields... } }
  -> { "task": "<task name>", "data": <task-specific result> }
```

```bash
curl -X POST http://localhost:3100/api/v1/process \
  -H "Content-Type: application/json" \
  -H "x-api-key: eupm_live_xxxx" \
  -d '{
    "task": "moderate",
    "input": { "sentence": "This product is garbage and the seller is a fucking idiot." }
  }'
```

```json
{
  "task": "moderate",
  "data": {
    "offensive": true,
    "alternatives": ["...", "...", "...", "...", "...", "..."]
  }
}
```

For `moderate_more`, `input` is `{ "sentence": "...", "existing": ["..."] }`
— see the `POST /moderate/more` section below for field constraints; they're
identical here.

An unrecognized or missing `task` returns `400` with the current
`supportedTasks` list, so integrations can discover what's available without
hardcoding it.

The task-specific routes below (`/moderate`, `/moderate/more`) remain
supported for existing integrations — they're equivalent to `/process` with
that task name, just with input fields at the body's top level and an
unwrapped response instead of the `{ task, data }` envelope.

### `POST /moderate`

Classify a single sentence (max 500 characters).

```bash
curl -X POST http://localhost:3100/api/v1/moderate \
  -H "Content-Type: application/json" \
  -H "x-api-key: eupm_live_xxxx" \
  -d '{"sentence": "This product is garbage and the seller is a fucking idiot."}'
```

Not offensive:

```json
{ "offensive": false }
```

Offensive:

```json
{
  "offensive": true,
  "alternatives": [
    "This product is disappointing and the seller was unprofessional.",
    "I'm unhappy with this product and how the seller handled it.",
    "This product didn't meet my expectations and the seller's conduct was poor.",
    "The product fell short of my expectations and the seller was difficult to work with.",
    "I'm not satisfied with this product or the seller's service.",
    "This purchase was a letdown, and the seller's behavior made it worse."
  ]
}
```

`alternatives` always has exactly 6 entries — enough that most integrations
never need to call `/moderate/more` at all.

### `POST /moderate/more`

Get 3 *additional* alternatives beyond the 6 already returned by
`/moderate`, distinct from ones already shown. Optional — only call this if
your UI needs even more options than the initial 6.

```bash
curl -X POST http://localhost:3100/api/v1/moderate/more \
  -H "Content-Type: application/json" \
  -H "x-api-key: eupm_live_xxxx" \
  -d '{
    "sentence": "This product is garbage and the seller is a fucking idiot.",
    "existing": [
      "This product is disappointing and the seller was unprofessional."
    ]
  }'
```

`existing` is optional (max 12 entries, 500 chars each) — pass the
alternatives already shown to the user so the new ones don't repeat them.

```json
{
  "alternatives": [
    "This product did not work as advertised, and I found the seller hard to deal with.",
    "I regret this purchase — the product and the seller's attitude both let me down.",
    "The product quality was poor and the seller was rude in our exchange."
  ]
}
```

## JavaScript example

Using the universal `/process` endpoint — the same `callTask` helper works
for any task, present or future, just by changing the `task` string:

```js
async function callTask(task, input) {
  const res = await fetch('http://localhost:3100/api/v1/process', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.EUPHEMIST_API_KEY,
    },
    body: JSON.stringify({ task, input }),
  });

  const body = await res.json().catch(() => ({}));
  if (!res.ok) {
    throw new Error(body.error || `Request failed: ${res.status}`);
  }

  return body.data;
}

// callTask('moderate', { sentence })
// callTask('moderate_more', { sentence, existing })
```

## Errors

| Status | Meaning |
|---|---|
| 400 | Missing/invalid `sentence` (or `existing`) in the request body |
| 401 | Missing or invalid `x-api-key` |
| 429 | Rate limit exceeded for this key |

The API never returns a 5xx for provider failures — it always falls back to
a locally-generated result so integrations don't have to build extra retry
logic for LLM outages.

## Managing keys

```bash
npm run create-key -- "Acme Inc"   # issue a new key

# in Docker, run it inside the container so the key lands on the
# mounted volume and survives restarts:
docker compose exec app node scripts/create-key.js "Acme Inc"
```

To revoke a key, open the key store and set `"revoked": true` on that key's
entry — it takes effect on the next request, no restart needed. The store is
`keys.json` next to `server.js` by default (git-ignored); set `KEYS_FILE` to
put it elsewhere. Under Docker it lives on the `euphemist-keys` volume at
`/app/data/keys.json`, editable with:

```bash
docker compose exec app vi /app/data/keys.json
```
