# Gateway HTTP API

> Standalone Hermes sidecar endpoints: GET /health, POST /recall, /capture, /search/memories, /search/conversations, /session/end, /seed — request/response fields and auth.

- Repository: TencentCloud/TencentDB-Agent-Memory
- GitHub: https://github.com/TencentCloud/TencentDB-Agent-Memory
- Human docs: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-14fefdd76c97
- Complete Markdown: https://grok-wiki.com/public/docs/tencentcloud-tencentdb-agent-memory-14fefdd76c97/llms-full.txt

## Source Files

- `src/gateway/server.ts`
- `src/gateway/types.ts`
- `src/gateway/config.ts`
- `hermes-plugin/memory/memory_tencentdb/client.py`
- `src/adapters/standalone/host-adapter.ts`

---

---
title: "Gateway HTTP API"
description: "Standalone Hermes sidecar endpoints: GET /health, POST /recall, /capture, /search/memories, /search/conversations, /session/end, /seed — request/response fields and auth."
---

The **TDAI Gateway** (`TdaiGateway` in `src/gateway/server.ts`) is a Node.js native `http` sidecar that exposes TDAI Core over JSON HTTP for standalone and Hermes hosts. Default bind is `http://127.0.0.1:8420`. It does not use Express or Fastify. Hermes talks to it through `MemoryTencentdbSdkClient` (`hermes-plugin/memory/memory_tencentdb/client.py`); OpenClaw uses the in-process plugin path and does not require this API.

```mermaid
flowchart LR
  subgraph Host["Host process"]
    H["Hermes MemoryTencentdbProvider"]
    C["MemoryTencentdbSdkClient"]
    S["GatewaySupervisor"]
    H --> C
    H --> S
  end
  subgraph GW["TdaiGateway :8420"]
    R["Route handlers"]
    A["StandaloneHostAdapter"]
    Core["TdaiCore"]
    R --> Core
    Core --> A
  end
  subgraph Store["Data dir"]
    D["~/.memory-tencentdb/memory-tdai\nor TDAI_DATA_DIR"]
  end
  C -->|"HTTP JSON\nBearer optional"| R
  S -->|"spawn / health"| GW
  Core --> D
```

## Endpoint inventory

| Method | Path | Auth when `apiKey` set | Purpose |
| :--- | :--- | :--- | :--- |
| `GET` | `/health` | Never | Liveness / store readiness |
| `OPTIONS` | `*` | Never | CORS preflight → `204` |
| `POST` | `/recall` | Yes | Prefetch memory context for a turn |
| `POST` | `/capture` | Yes | Record a user/assistant turn (L0 + pipeline notify) |
| `POST` | `/search/memories` | Yes | Search L1 structured memories |
| `POST` | `/search/conversations` | Yes | Search L0 raw conversations |
| `POST` | `/session/end` | Yes | End session and flush pipeline work |
| `POST` | `/seed` | Yes | Batch seed historical conversations (blocking) |

All successful responses use `Content-Type: application/json`. Unknown routes return `404` with `{ "error": "Not found: METHOD /path" }`. Uncaught handler errors return `500` with `{ "error": "<message>" }`. Invalid JSON body fails with `500` and message `Invalid JSON body` (parse rejection is not mapped to `400`).

## Runtime and config surface

| Concern | Default | Config / env |
| :--- | :--- | :--- |
| Port | `8420` | `server.port` / `TDAI_GATEWAY_PORT` |
| Host | `127.0.0.1` | `server.host` / `TDAI_GATEWAY_HOST` |
| API key | unset (auth off) | `server.apiKey` / `TDAI_GATEWAY_API_KEY` |
| CORS allow-list | `[]` (no CORS headers) | `server.corsOrigins` / `TDAI_CORS_ORIGINS` |
| Data dir | `$MEMORY_TENCENTDB_ROOT/memory-tdai` or `~/.memory-tencentdb/memory-tdai` (legacy `~/memory-tdai` if present) | `data.baseDir` / `TDAI_DATA_DIR` |
| LLM | OpenAI-compatible defaults | `llm.*` / `TDAI_LLM_*` |
| Memory plugin config | Same schema as OpenClaw plugin | `memory` block via `parseConfig` |

Config file resolution order:

1. `TDAI_GATEWAY_CONFIG` (explicit path)
2. `./tdai-gateway.yaml` or `./tdai-gateway.json` in CWD
3. `<dataDir>/tdai-gateway.yaml` or `.json`
4. Environment-only config

String leaves of the form `${VAR}` expand from the process environment. Malformed config files fall back to env-only defaults.

Start (manual):

```bash
npx tsx src/gateway/server.ts
# or via Hermes supervisor / MEMORY_TENCENTDB_GATEWAY_CMD
```

Hermes default client base URL: `http://127.0.0.1:8420`. Client default timeout: **10s** (`DEFAULT_TIMEOUT`); health probes often use **2–3s**; seed uses **300s**.

## Authentication

Auth is **opt-in**. When `server.apiKey` / `TDAI_GATEWAY_API_KEY` is unset, every route is open (legacy default). Startup logs:

- `Security posture: auth=disabled|ENABLED (Bearer) host=... cors=...`
- `WARN` if auth is off
- Louder `WARN` if bound to a non-loopback host without an API key
- `WARN` if CORS allow-list contains `*`

When an API key **is** set:

- `GET /health` and `OPTIONS` stay open
- All other routes require `Authorization: Bearer <apiKey>`
- Missing/malformed header → `401` `{ "error": "Unauthorized: missing Bearer token" }`
- Wrong token → `401` `{ "error": "Unauthorized: invalid token" }`
- Comparison uses constant-time equality (`crypto.timingSafeEqual`) after length check

### Client-side keys (Hermes)

| Process | Variable | Role |
| :--- | :--- | :--- |
| Gateway (server) | `TDAI_GATEWAY_API_KEY` or `server.apiKey` | Enforces Bearer check |
| Hermes client | `MEMORY_TENCENTDB_GATEWAY_API_KEY`, fallback `TDAI_GATEWAY_API_KEY` | Attaches Bearer on outbound calls |

The supervisor does **not** inject the client key into the Gateway process. Operators must set the same secret on both sides when auth is enabled.

```bash
curl -s http://127.0.0.1:8420/health

curl -s -X POST http://127.0.0.1:8420/recall \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
  -d '{"query":"...","session_key":"..."}'
```

## CORS

| `corsOrigins` | Behavior |
| :--- | :--- |
| `[]` (default) | No `Access-Control-Allow-*` headers; browsers block cross-origin use |
| concrete origins | Echo matching `Origin`; set methods `GET, POST, OPTIONS`, headers `Content-Type, Authorization`, and `Vary: Origin` |
| `["*"]` | `Access-Control-Allow-Origin: *` (dev only; startup WARN) |

`OPTIONS` always answers `204` after CORS header application (no auth gate).

## Hermes lifecycle mapping

| Hermes / provider call | Gateway endpoint | Notes |
| :--- | :--- | :--- |
| `prefetch(query)` | `POST /recall` | Synchronous; inject returned `context` |
| `sync_turn(user, assistant)` | `POST /capture` | Often fire-and-forget on a background thread |
| LLM tools (memory / conversation search) | `POST /search/memories`, `POST /search/conversations` | Via `MemoryTencentdbSdkClient` |
| `on_session_end` / shutdown flush | `POST /session/end` | Flush pending pipeline work |
| Batch import (optional) | `POST /seed` | Not the primary turn path; long-running |

Provider-side reliability (circuit breaker, capture back-pressure, supervisor) lives in the Hermes plugin, not in the HTTP handlers themselves.

---

## Endpoints

:::endpoint GET /health Liveness and store readiness
Always public. Used by `GatewaySupervisor`, Docker/k8s probes, and `memory-tencentdb-ctl health`.

**Response `200`**

| Field | Type | Description |
| :--- | :--- | :--- |
| `status` | `"ok"` \| `"degraded"` | `"ok"` when vector store is present; otherwise `"degraded"` |
| `version` | string | Gateway version constant (`0.1.0` in current source) |
| `uptime` | number | Seconds since listen |
| `stores.vectorStore` | boolean | `TdaiCore.getVectorStore()` truthy |
| `stores.embeddingService` | boolean | `TdaiCore.getEmbeddingService()` truthy |

<ResponseExample>
```json
{
  "status": "ok",
  "version": "0.1.0",
  "uptime": 42,
  "stores": {
    "vectorStore": true,
    "embeddingService": true
  }
}
```
</ResponseExample>
:::

:::endpoint POST /recall Prefetch memory context for the next turn
Calls `TdaiCore.handleBeforeRecall(query, session_key)`.

<ParamField body="query" type="string" required>
Recall query text (usually the latest user message).
</ParamField>
<ParamField body="session_key" type="string" required>
Session scope key.
</ParamField>
<ParamField body="user_id" type="string">
Optional; accepted by types and Hermes client. Current handler does not pass it into core.
</ParamField>

**Errors:** `400` if `query` or `session_key` missing.

**Response `200`**

| Field | Type | Description |
| :--- | :--- | :--- |
| `context` | string | System context to inject (`appendSystemContext`, may be empty) |
| `strategy` | string? | Recall strategy used |
| `memory_count` | number? | Count of recalled L1 memories |

<RequestExample>
```bash
curl -s -X POST http://127.0.0.1:8420/recall \
  -H "Content-Type: application/json" \
  -d '{"query":"What did we decide about storage?","session_key":"sess-1"}'
```
</RequestExample>

<ResponseExample>
```json
{
  "context": "...memory context...",
  "strategy": "hybrid",
  "memory_count": 3
}
```
</ResponseExample>
:::

:::endpoint POST /capture Record a conversation turn
Calls `TdaiCore.handleTurnCommitted`. If `messages` is omitted, the handler synthesizes a two-message array from `user_content` / `assistant_content`.

<ParamField body="user_content" type="string" required>
User turn text.
</ParamField>
<ParamField body="assistant_content" type="string" required>
Assistant turn text.
</ParamField>
<ParamField body="session_key" type="string" required>
Session scope key.
</ParamField>
<ParamField body="session_id" type="string">
Optional session id passed through to core.
</ParamField>
<ParamField body="user_id" type="string">
Optional; accepted by types/client; not applied by the current handler.
</ParamField>
<ParamField body="messages" type="unknown[]">
Optional full message list; defaults to user + assistant pair built from content fields.
</ParamField>

**Errors:** `400` if any of `user_content`, `assistant_content`, `session_key` missing.

**Response `200`**

| Field | Type | Description |
| :--- | :--- | :--- |
| `l0_recorded` | number | L0 records written this turn |
| `scheduler_notified` | boolean | Whether the extraction pipeline scheduler was notified |

<RequestExample>
```bash
curl -s -X POST http://127.0.0.1:8420/capture \
  -H "Content-Type: application/json" \
  -d '{
    "user_content": "Remember I prefer SQLite for local dev",
    "assistant_content": "Noted — local SQLite, TCVDB for prod.",
    "session_key": "sess-1"
  }'
```
</RequestExample>

<ResponseExample>
```json
{
  "l0_recorded": 1,
  "scheduler_notified": true
}
```
</ResponseExample>
:::

:::endpoint POST /search/memories Search L1 structured memories
Calls `TdaiCore.searchMemories`. Hermes client default `limit` is `5`.

<ParamField body="query" type="string" required>
Search query.
</ParamField>
<ParamField body="limit" type="number">
Max results.
</ParamField>
<ParamField body="type" type="string">
Optional L1 type filter.
</ParamField>
<ParamField body="scene" type="string">
Optional scene filter.
</ParamField>

**Errors:** `400` if `query` missing.

**Response `200`**

| Field | Type | Description |
| :--- | :--- | :--- |
| `results` | string | Formatted result text from core |
| `total` | number | Hit count |
| `strategy` | string | Search strategy used |
:::

:::endpoint POST /search/conversations Search L0 raw conversations
Calls `TdaiCore.searchConversations`.

<ParamField body="query" type="string" required>
Search query.
</ParamField>
<ParamField body="limit" type="number">
Max results.
</ParamField>
<ParamField body="session_key" type="string">
Optional session scope for the search.
</ParamField>

**Errors:** `400` if `query` missing.

**Response `200`**

| Field | Type | Description |
| :--- | :--- | :--- |
| `results` | string | Formatted result text |
| `total` | number | Hit count |
:::

:::endpoint POST /session/end End session and flush
Calls `TdaiCore.handleSessionEnd(session_key)`. Response always reports success once the call returns.

<ParamField body="session_key" type="string" required>
Session to flush/end.
</ParamField>
<ParamField body="user_id" type="string">
Optional; accepted by types/client; not applied by the current handler.
</ParamField>

**Errors:** `400` if `session_key` missing.

**Response `200`**

| Field | Type | Description |
| :--- | :--- | :--- |
| `flushed` | boolean | Always `true` on successful completion |
:::

:::endpoint POST /seed Batch seed historical conversations
Validates input with the same seed pipeline as CLI seed (`validateAndNormalizeRaw` / `executeSeed`). **Blocking** — large inputs may take minutes. Hermes client default timeout is **300s**.

Output is written under a timestamped directory:

`{data.baseDir}/seed-YYYYMMDD-HHMMSS`

Gateway injects its LLM settings into the seed `pluginConfig` (`llm.enabled=true` plus baseUrl/apiKey/model/maxTokens/timeoutMs/disableThinking). Optional `config_override` is deep-merged one level per top-level key.

<ParamField body="data" type="object | array" required>
Seed payload — Format A `{ "sessions": [...] }` or Format B `[...]` (same shapes as `openclaw memory-tdai seed --input`).
</ParamField>
<ParamField body="session_key" type="string">
Fallback session key when input sessions omit one.
</ParamField>
<ParamField body="strict_round_role" type="boolean">
Require each round to include both user and assistant (default false).
</ParamField>
<ParamField body="auto_fill_timestamps" type="boolean">
Auto-fill missing timestamps (default **true**).
</ParamField>
<ParamField body="config_override" type="object">
Plugin config overrides deep-merged onto gateway memory + injected LLM config.
</ParamField>

**Errors:**

- `400` `{ "error": "Missing required field: data" }`
- `400` validation failure: `{ "error": "<message>", "validation_errors": [...] }` (`SeedValidationError`)

**Response `200`**

| Field | Type | Description |
| :--- | :--- | :--- |
| `sessions_processed` | number | Sessions completed |
| `rounds_processed` | number | Rounds completed |
| `messages_processed` | number | Messages completed |
| `l0_recorded` | number | L0 records written |
| `duration_ms` | number | Wall time |
| `output_dir` | string | Seed output directory path |

<RequestExample>
```bash
curl -s -X POST http://127.0.0.1:8420/seed \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "sessions": [{
        "sessionKey": "import-1",
        "conversations": [[
          {"role":"user","content":"Hello"},
          {"role":"assistant","content":"Hi"}
        ]]
      }]
    },
    "auto_fill_timestamps": true
  }'
```
</RequestExample>
:::

## Error envelope

Standard error body (`GatewayErrorResponse`):

```json
{ "error": "string", "code": "optional string" }
```

`code` is defined on the type but handlers typically set only `error`. Seed validation adds `validation_errors` outside that minimal shape.

| Status | When |
| :--- | :--- |
| `400` | Missing required fields; seed validation failure |
| `401` | Auth enabled and Bearer missing/invalid |
| `404` | Unknown method/path |
| `500` | Uncaught errors, including invalid JSON body |

## Host adapter boundary

`TdaiGateway` constructs `StandaloneHostAdapter` with:

- `dataDir` → gateway `data.baseDir`
- `llmConfig` → gateway `llm`
- `platform: "gateway"`
- default `userId`: `"default_user"`

Per-request handlers currently scope work primarily via `session_key` (and capture `session_id` / message payload). This is the standalone/Hermes path; OpenClaw uses a different host adapter and never needs these HTTP routes for normal plugin operation.

## Smoke checks

```bash
# Health (always works without Bearer)
curl -s http://127.0.0.1:8420/health | python3 -m json.tool

# Recall with auth when configured
curl -s -X POST http://127.0.0.1:8420/recall \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TDAI_GATEWAY_API_KEY" \
  -d '{"query":"test","session_key":"debug"}'
```

Expect health `status` of `"ok"` or `"degraded"`. On Hermes, supervisor treats either as “running”. Startup logs with tag `[tdai-gateway]` should show listen address and security posture.

## Failure modes (API-relevant)

| Symptom | Likely cause | Probe |
| :--- | :--- | :--- |
| Connection refused | Gateway not started / wrong port | Supervisor, `MEMORY_TENCENTDB_GATEWAY_CMD`, port env |
| `401` on POST | Client missing Bearer while Gateway has `apiKey` | Align `TDAI_GATEWAY_API_KEY` and `MEMORY_TENCENTDB_GATEWAY_API_KEY` |
| Health `"degraded"` | Vector store not ready | `stores` object in `/health`; storage config |
| Hermes pauses calls | Provider circuit breaker (plugin-side) | Hermes provider logs; not an HTTP status from Gateway |
| Seed times out | Large batch + default 300s client timeout | Increase client timeout; run offline seed CLI instead |
| Non-loopback + no auth | Misconfiguration exposure | Startup WARN; bind `127.0.0.1` or set API key |

## Related pages

<CardGroup>
  <Card title="Install on Hermes" href="/install-hermes">
    Provider install, Gateway supervision, and config.yaml keys.
  </Card>
  <Card title="Gateway control" href="/gateway-control">
    memory-tencentdb-ctl start/stop/status/health/logs and path overrides.
  </Card>
  <Card title="Host adapters" href="/host-adapters">
    StandaloneHostAdapter vs OpenClaw adapter boundary.
  </Card>
  <Card title="Seed historical conversations" href="/seed-conversations">
    Format A/B seed input and L0→L3 verification (CLI and /seed parity).
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    Full memory.* schema used under gateway memory config.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    Gateway circuit breaker and recovery probes from the host side.
  </Card>
</CardGroup>
