# donto API reference

donto is a contradiction-preserving CLAIM/DISCOVERY substrate: a bitemporal, paraconsistent, evidence-first knowledge base that holds incompatible claims forever as legal state, anchors each to its source, links them with typed argument edges, and re-ranks by reality over time instead of deleting on conflict. This document is the developer-facing reference for donto's **programmatic surfaces** — every HTTP API you can use to read, write, and query the substrate, plus its MCP (Model Context Protocol) tools. It covers five services (a memory API, a distributed embedding coordinator, a genealogy extraction/ingest API, the core substrate sidecar, and a read-only admin console), the authentication model, the shared bitemporal data model, and end-to-end quickstart recipes.

---

## Overview

| Service | Public base URL | Purpose | Auth |
|---|---|---|---|
| **donto-memory** | `https://memories.apexpots.com` | Agentic episodic + semantic memory: memorize, recall, substrate-wide search, module ingest, reconsolidation, operator observability. | Public for the agent contract; operator routes (`/jobs/*`, `/explore/*`) gated by an optional bearer token. |
| **donto-embed-coordinator** | `https://donto.org/embed` (also `https://memories.apexpots.com/embed`) | Distributed embedding work queue: enqueue items, lease batches, submit computed vectors. Lets spare machines/GPUs embed at billion scale. | Bearer token on every endpoint except `/embed/health`. |
| **donto-api** (genealogy) | `https://genes.apexpots.com` | LLM extraction + ingest into the knowledge graph, plus rich read/search/graph/alignment endpoints over the substrate. | Public (no auth). |
| **dontosrv** (core substrate) | `https://genes.apexpots.com` (internal `localhost:7879`) | The core bitemporal claim substrate HTTP sidecar: ~70 endpoints for the claim write-path, read/search, evidence, trust kernel, arguments, alignment, shapes/rules, and schema discovery. | Public reads; writes ungated at the HTTP layer (policy gates applied at the SQL layer per context/action). |
| **donto-admin** | `https://admin.donto.org` | Read-only observability console: substrate metrics, contexts, fabric, processes, ingestion jobs, per-job charts/nebula/panels, relation introspection, BEAM progress. | Read-only Postgres role (`donto_ro`); no per-request credentials. |

> **donto-memory-mcp** is not a separate HTTP service — it is the MCP front-end over donto-memory's `/recall`, `/search`, and `/memorize` endpoints. See [MCP access](#mcp-access).

---

## Authentication

donto's programmatic surfaces use three distinct auth postures. There is no per-request API key for the public substrate — the substrate is openly readable and (for the genealogy/substrate write-path) openly writable by design.

### The token model

1. **Public endpoints (no auth).** Most of the agent contract is public: donto-memory's `/health`, `/version`, `/modules`, `/memorize`, `/recall`, `/search`, `/search/resources`, `/ingest`, `/reconsolidate/*`; all of donto-api; all of dontosrv reads/writes. Just send the request.

2. **Bearer token (embed coordinator + optional memory ops).**
   - **donto-embed-coordinator** requires `Authorization: Bearer <TOKEN>` on every endpoint **except** `/embed/health`. The token is read from the `DONTO_EMBED_TOKEN` environment variable, which is sourced from the secret file `/etc/donto/embed-coordinator.token` (server-side; never expose it). A missing or invalid token returns `401 Unauthorized`. If the env var is unset, the service runs open (dev mode only).
   - **donto-memory operator routes** (`/jobs`, `/jobs/list.json`, `/jobs/:id`, `/jobs/:id/raw`, `/explore`, `/explore/*.json`) require `Authorization: Bearer <TOKEN>` **only if** `DONTO_MEMORY_OPS_TOKEN` is set on the server. If it is unset, these routes are a pass-through (local dev). The `/api` endpoint's `ops_token_required` field tells you which mode is live.

3. **Read-only Postgres role (admin console).** donto-admin requires no per-request credentials; access is enforced at the database level. It connects with the `donto_ro` role (via the server-side `DONTO_RO_DSN` env var), which is locked to `SELECT`-only with `default_transaction_read_only=on`.

### Where keys live (by reference, never inline)

| Secret | Server-side location | Used by |
|---|---|---|
| Embed coordinator bearer token | `/etc/donto/embed-coordinator.token` (env `DONTO_EMBED_TOKEN`, from `/etc/donto/embed-coordinator.env`) | donto-embed-coordinator |
| Memory ops token | env `DONTO_MEMORY_OPS_TOKEN` (set on the donto-memory server if gating is enabled) | donto-memory operator routes |
| Read-only DSN | env `DONTO_RO_DSN` (defaults to a `donto_ro@127.0.0.1:5432/donto` connection string) | donto-admin |

Never paste the literal secret values into requests, configs, or logs. Pass tokens only as `Authorization: Bearer <TOKEN>`, substituting the real value from the secret store at runtime.

### Passing tokens

```
Authorization: Bearer <TOKEN>
```

### The Cloudflare User-Agent gotcha

All public domains are proxied through Cloudflare. Cloudflare blocks some default client User-Agents (e.g. plain `python-urllib`) with a `403` / Cloudflare error `1010`. **Set a non-default User-Agent such as `curl/8`** — `curl`, `reqwest`, and `aiohttp` pass by default, but if you build raw requests, add a `User-Agent` header:

```bash
curl -A 'curl/8' https://donto.org/embed/health
```

This is required against `donto.org` / `memories.apexpots.com` / `genes.apexpots.com` whenever your client library would otherwise send a blocked UA.

---

## Conventions

### Base URLs

Use the public base URLs from the [Overview](#overview) table. Internal ports (`:7900`, `:7930`, `:7879`, `:8000`, `:3103`, `:8233`) are reachable only on the host and are noted for operators; external developers use the HTTPS domains.

### Content type

All request bodies and responses are JSON unless noted. Send `Content-Type: application/json` on POSTs with a body. A handful of endpoints return other media types (text/plain for `/metrics`, `/health` on dontosrv, the DontoQL grammar; text/markdown for `/agent.md`; HTML for `/jobs`, `/explore`, `/docs`; Server-Sent Events for `/firehose/stream`) — these are called out per endpoint.

### The bitemporal / context data model

A caller must understand a few substrate concepts that recur in every payload:

- **Statement (claim).** The atomic unit: `subject predicate object` in a `context`. Objects are either an IRI (`object_iri`) or a literal (`object_lit`, an object `{v: value, dt: xsd-type, lang?: language}` — e.g. `{"v":"1860-01-15","dt":"xsd:date"}`). Subject and object exactly one of IRI/literal.
- **IRIs.** Subjects are minted `ex:kebab-case` (e.g. `ex:mary-watson`). Predicates are `camelCase` (e.g. `bornIn`, `marriedTo`). Contexts are `ctx:namespace/topic/source` (e.g. `ctx:genes/mary-watson/obituary`) or `donto:...` / `mem:...` namespaced IRIs. Memory holders are IRIs like `agent:my-bot` or `user:alice`; sessions like `discord:user:12345`. Predicates are freely minted by extractors (abundance) — variants are reconciled at query time by the alignment engine, not by a fixed schema.
- **Contexts.** Named scopes (sources, extraction runs, research questions). Facts are scoped per context, and contexts can carry policies. Examples: `ctx:genes/<topic>/<source>`, `ctx:extract/<file>/<model>`, `mem:module/episodic`.
- **Bitemporality.** Every statement carries two time axes:
  - **transaction time** (`tx_time`, exposed as `tx_lo`/`tx_hi`): when the system recorded / retracted the fact. An open interval (`tx_hi`/`upper(tx_time)` is null) means the fact is currently believed; a closed interval means it was retracted (soft-deleted, never destroyed).
  - **valid time** (`valid_from`/`valid_to`, exposed as `valid_lo`/`valid_hi`): when the fact became / ceased being true in the world. Distinct from when it was recorded.
  - **Time-travel:** read endpoints accept `as_of_tx` (RFC3339) to ask "what did we believe on this past date?" and `as_of_valid` (date) for valid-time slicing.
- **Polarity.** Statements are `asserted` | `negated` | `absent` | `unknown`. (In raw `flags`, the low 2 bits encode polarity: 0=asserted, 1=negated, 2=absent, 3=unknown.)
- **Maturity (the E0–E4 / 0–4 ladder).** `0`=L0 raw, `1`=L1 registered, `2`=L2 evidenced, `3`=L3 validated, `4`=L4 certified. Higher maturity is prioritized in queries. (Admin charts may show buckets `0–7`.)
- **Evidence anchoring.** Facts link to sources via `donto_evidence_link → donto_span → donto_document_revision → blob`. A span carries `surface_text` (the anchored snippet) plus character offsets. Extraction and anchoring are separate stages; anchorless facts are held honestly rather than given bogus spans.
- **Identity-as-hypothesis.** Subjects may be the "same" entity under an identity lens (`lens_name`); resolution happens at query time, not by destructive merge.

### Error shape

Errors vary slightly by service:

- **dontosrv / donto-api / donto-embed-coordinator:** a JSON object with an `error` field, e.g. `{ "error": "..." }`, possibly with `detail` / `reason`.
- **donto-admin:** a uniform envelope `{ "ok": false, "error": string, "at": ISO8601 }`, HTTP 500 unless noted. Successful admin responses are `{ "ok": true, "data": T, "at": ISO8601 }`, never cached (`cache-control: no-store`).
- **HTTP statuses:** `400` bad input (empty query, missing/invalid fields, unknown module), `401` missing/invalid token (embed coordinator, gated memory ops), `404` not found (record, job, revision, unknown module/panel), `500` substrate/DB/extraction failure, `202` async deferred (extraction queued). Search endpoints degrade gracefully: on a 9s timeout they return `partial: true` with partial results instead of failing.

---

## donto-memory

`https://memories.apexpots.com` — an agentic episodic + semantic memory substrate. It stores raw text (**episodic**) alongside LLM-extracted structured claims (**semantic**) into the bitemporal substrate, scoped by **holder** (agent IRI, for recall isolation) and optionally **session**. Memory is composed over `mem:module/*` contexts and stored as `donto_statement` rows.

Extraction modes: `single` (one LLM call, ~20–100 facts), `exhaustive` (five parallel apertures, ~80–250 facts, ~5× token cost), `deep` (N sequential passes, default 3, max 10, dedup at end). The default mode is the server's `DONTO_MEMORY_EXTRACT_MODE` (defaults to `exhaustive`). Slow modes (`deep`, `exhaustive`) return `202 Accepted` and run on a Temporal durable queue (with a tokio fallback); fast modes return `200`.

OpenAPI spec: `https://memories.apexpots.com/openapi.json`. Swagger UI: `https://memories.apexpots.com/docs`.

> **Caveats (live-verified 2026-06-07):** `POST /search` and `POST /search/resources` are served and return real data, but are **absent from the published `openapi.json`** — don't rely on the spec alone to discover them (they're documented below). Operator routes (`/jobs/*`, `/explore/*`) are gated only when `DONTO_MEMORY_OPS_TOKEN` is set; the **currently deployed instance reports `ops_token_required: false`** (ungated), so those routes answer without a token right now — check `GET /api` for the live mode rather than assuming a 401.

### GET /health

Health check. Auth: none.

- **Request:** no parameters.
- **Response:** `{status: string}` — `{"status":"ok"}` if running.

```bash
curl https://memories.apexpots.com/health
```

### GET /version

Service version and configuration. Auth: none.

- **Request:** no parameters.
- **Response:** `{service: string, version: string, substrate_url: string, substrate_contract_floor: string}`.

```bash
curl https://memories.apexpots.com/version
```

### GET /substrate

Substrate health + contract details. Auth: none.

- **Request:** no parameters.
- **Response:** `{contract: {actions: [...], ...}, health: {...}, substrate_contract_floor: string}`. Runs `contract_version()` and `substrate_health()` in parallel with an 8s timeout; `health` degrades gracefully if substrate counts are slow.

```bash
curl https://memories.apexpots.com/substrate
```

### GET /api

API summary and endpoint listing. Auth: none.

- **Request:** no parameters.
- **Response:** `{service, version, substrate_contract_floor, ops_token_required: boolean, async_memorize_queue?: {pending, completed_24h, failed_24h, lost_24h, drain_safe}, endpoints: {agent_contract: string[], documentation: string[], operator: string[]}}`. `ops_token_required` tells you whether operator routes are gated.

```bash
curl https://memories.apexpots.com/api
```

### GET /modules

List memory modules. Auth: none.

- **Request:** no parameters.
- **Response:** `{modules: [{module_iri, label, description, form, function, version, source, enabled_in_db?: boolean}]}`. Standard modules: `mem:module/episodic`, `mem:module/semantic-claim`, `mem:module/preference`.

```bash
curl https://memories.apexpots.com/modules
```

### POST /memorize

Save an episodic chunk + LLM-extracted semantic claims. Auth: none.

- **Request body:**
  - `holder` (string, required) — agent IRI scope, e.g. `agent:my-bot`.
  - `session_id` (string, optional) — e.g. `discord:user:12345`.
  - `text` (string) — the chunk (required unless `images` provided).
  - `modality` (string, optional, default `model_output`).
  - `extract` (boolean, optional, default `true`) — extract semantic claims via LLM.
  - `mode` (string, optional) — `single` | `exhaustive` | `deep`.
  - `passes` (u32, optional) — for `mode=deep` (1–10, default 3).
  - `async` (boolean, optional) — force deferred execution.
  - `images` (string[], optional) — `http(s)` URLs or `data:image/...;base64`; OCR transcription is prepended to episodic text when enabled.
  - `facts` (ExtractedFact[], optional) — pre-extracted facts from upstream.
  - `valid_from` (string, optional) — `YYYY-MM-DD` or RFC3339; when the fact became true in the world (distinct from `tx_time`).
- **Response (200 sync, or 202 async):** `{holder, session_id, episodic_record_id: uuid, episodic_record_iri, extracted: boolean, extract_mode, facts_extracted, facts_ingested, dedup_collisions, semantic_record_ids: uuid[], model?, usage?: {completion_tokens, input_tokens, output_tokens}, aperture_yields: [], facts: ExtractedFact[], elapsed_ms, warnings: string[], queue_id?: uuid}`. On 202: `{status:"queued", queue_id, durable: boolean, holder, session_id, extract_mode, passes, note}`.
- **Behavior:** async (`202`) when `mode` is `deep`/`exhaustive`/`opencode` or `async=true`; else sync (`200`). Poll `/jobs` or `/recall` for async results.

```bash
curl -X POST https://memories.apexpots.com/memorize \
  -H 'Content-Type: application/json' \
  -d '{"holder":"agent:my-bot","session_id":"discord:user:12345","text":"I met Annie Davis in 1979."}'
```

### POST /memorize/batch

Bulk memorize. Auth: none.

- **Request body:** `{items: MemorizeReq[]}` — an array of `/memorize` request bodies. Processed sequentially.
- **Response:** `{results: object[]}` — each is a `MemorizeResp` (same fields as `/memorize`) or `{error, holder, text_preview}`.

```bash
curl -X POST https://memories.apexpots.com/memorize/batch \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"holder":"agent:my-bot","text":"First memory"},{"holder":"agent:my-bot","text":"Second memory"}]}'
```

### POST /recall

Holder-scoped episodic + semantic recall bundle (hybrid lexical + semantic, bitemporal). Auth: none.

- **Request body:**
  - `holder` (string, required) — scope isolation.
  - `query` (string, optional) — hybrid search term.
  - `limit` (number, optional, default 50; clamped 1–configured max).
  - `session_id` (string, optional).
  - `subject` / `predicate` / `object_iri` (string, optional) — exact IRI filters.
  - `action` (string, optional, default `read_content`) — policy action.
  - `polarity` (string, optional, default `asserted`).
  - `as_of_tx` (RFC3339, optional) — bitemporal time-travel.
  - `as_of_valid` (date, optional) — valid-time slice.
  - `lens_name` (string, optional) — identity hypothesis.
  - `min_maturity` (0–4, optional).
  - `module_iris` (string[], optional).
  - `permitted_only` (boolean, optional, default `true`).
- **Response:** `{holder, action, modules_used: string[], row_count, rows: [{statement_id, subject, predicate, object_iri, object_lit: {v, dt, lang?}, context, flags, maturity, valid_from, valid_to, as_of, provenance}], policy_report: {default_action, permitted_only}, lens?: string, as_of?: string}`. Composed over `mem:module/*` contexts; the policy report is always included.

```bash
curl -X POST https://memories.apexpots.com/recall \
  -H 'Content-Type: application/json' \
  -d '{"holder":"agent:my-bot","limit":10}'
```

### POST /search

Substrate-wide ranked full-text search over names. Auth: none (intentionally public).

- **Request body:**
  - `query` (string, required, non-empty) — lexemes are ANDed.
  - `context_prefix` (string, optional) — scope filter, `LIKE '%prefix%'` (e.g. `ctx:genes`).
  - `limit` (number, optional, default 50; clamped 1–500).
- **Response:** `{query, context_prefix, row_count, rows: [{statement_id, subject, predicate, object_iri, object_lit, context, score}], partial: boolean, elapsed_ms, note?}`. Searches IRI path segments, `rdfs:label` literals, and short literal prefixes across the entire substrate (≈39M rows), backed by the `donto_statement_fts_name` GIN index. Candidate set capped at 2000 rows; `partial=true` if the 9s timeout is exceeded; `score` is `ts_rank`.

```bash
curl -X POST https://memories.apexpots.com/search \
  -H 'Content-Type: application/json' \
  -d '{"query":"love","limit":3}'
```

### POST /search/resources

Full-text search over resource source bodies. Auth: none.

- **Request body:** `{query: string (required, non-empty), limit?: number (default 20, clamped 1–200)}`.
- **Response:** `{query, row_count, rows: [{document_id, revision_id, score, snippet, blob_uri, body_uri}], partial: boolean, elapsed_ms, note?}`. Searches `donto_document_revision.body` (raw source text); returns the latest revision per document, a highlighted snippet (max ~300 chars, `ts_headline` MaxFragments=1/MaxWords=30), `blob_uri` (`gs://apex-494316-donto/sha256/<hash>` for stored blobs) and `body_uri` (`file://` legacy). Candidate cap 300 rows.

```bash
curl -X POST https://memories.apexpots.com/search/resources \
  -H 'Content-Type: application/json' \
  -d '{"query":"love","limit":3}'
```

### POST /ingest/:module_iri

Direct module ingest (bypass LLM extraction). Auth: none.

- **Path param:** `module_iri` — `mem:module/episodic` (text), `mem:module/semantic-claim` (structured claims), `mem:module/preference` (key-value). Short names are canonicalized (`episodic` → `mem:module/episodic`).
- **Request body:** `IngestInput` `{holder?, session_id?, ...module-specific fields}`.
- **Response:** `{record_id: uuid, record_iri, module_iri, anchored_to: {statement_id: uuid, frame_id: uuid, context_iri}}`. `404` if module not registered, `400` for invalid input.

```bash
curl -X POST 'https://memories.apexpots.com/ingest/mem:module/episodic' \
  -H 'Content-Type: application/json' \
  -d '{"holder":"agent:my-bot","session_id":"test:1","text":"Raw episodic chunk"}'
```

### POST /reconsolidate/enqueue

Enqueue a record for reconsolidation. Auth: none.

- **Request body:** `{record_id: uuid (required), reason?: string (default `explicit`), priority?: f64}`. Higher priority is processed first (default 0.0).
- **Response:** `{queue_id: uuid, record_id, reason, priority, available_at: RFC3339}`. `404` if record not found. Adds to `donto_x_memory_reconsolidation_queue` with a coalesce window.

```bash
curl -X POST https://memories.apexpots.com/reconsolidate/enqueue \
  -H 'Content-Type: application/json' \
  -d '{"record_id":"<uuid>","reason":"explicit","priority":1.0}'
```

### GET /reconsolidate/queue

List the reconsolidation queue. Auth: none.

- **Query params:** `limit?` (default 100, clamped 1–1000).
- **Response:** `{count, items: [{queue_id, record_id, reason, priority, available_at, claimed_at?, claimed_by?, completed_at?}]}`. Shows pending entries (`completed_at IS NULL`), sorted by priority DESC, `available_at` ASC.

```bash
curl 'https://memories.apexpots.com/reconsolidate/queue?limit=5'
```

### GET /jobs

Jobs list (HTML view). Auth: bearer token if `ops_token_required=true`.

- **Query params:** `endpoint?` (ilike filter), `holder?` (exact), `limit?` (default 100, clamped 1–1000).
- **Response:** `200 OK` HTML with a filterable audit table (from `donto_x_memory_job_log`).

```bash
curl -H 'Authorization: Bearer <TOKEN>' https://memories.apexpots.com/jobs
```

### GET /jobs/list.json

Jobs list (JSON). Auth: bearer token if `ops_token_required=true`.

- **Query params:** `endpoint?`, `holder?`, `limit?` (default 100, clamped 1–1000).
- **Response:** `{count, jobs: [{job_id, created_at, endpoint, holder, session_id, status_code, elapsed_ms, facts_extracted?, facts_ingested?, rows_returned?, model?, total_tokens?, error?}]}`.

```bash
curl -H 'Authorization: Bearer <TOKEN>' https://memories.apexpots.com/jobs/list.json
```

### GET /jobs/:id

Job detail (HTML). Auth: bearer token if `ops_token_required=true`.

- **Path param:** `id` (uuid).
- **Response:** `200 OK` HTML with the full job detail (request body, response body, metrics, elapsed time), or `404`.

```bash
curl -H 'Authorization: Bearer <TOKEN>' \
  https://memories.apexpots.com/jobs/550e8400-e29b-41d4-a716-446655440000
```

### GET /jobs/:id/raw

Job detail (JSON). Auth: bearer token if `ops_token_required=true`.

- **Path param:** `id` (uuid).
- **Response:** `JobDetail {job_id, created_at, endpoint, holder, session_id, status_code, elapsed_ms, request, response, facts_extracted?, facts_ingested?, rows_returned?, model?, prompt_tokens?, completion_tokens?, total_tokens?, error?}`, or `404`.

```bash
curl -H 'Authorization: Bearer <TOKEN>' \
  https://memories.apexpots.com/jobs/550e8400-e29b-41d4-a716-446655440000/raw
```

### GET /explore

Interactive memory explorer (HTML). Auth: bearer token if `ops_token_required=true`.

- **Request:** no parameters.
- **Response:** `200 OK` self-contained HTML/JS explorer that drills holder → sessions → records → facts via the `/explore/*.json` endpoints below.

```bash
curl -H 'Authorization: Bearer <TOKEN>' https://memories.apexpots.com/explore
```

### GET /explore/stats.json

Memory system statistics. Auth: bearer token if `ops_token_required=true`.

- **Response:** `{holders, sessions, records, modules, recall_events}`.

```bash
curl -H 'Authorization: Bearer <TOKEN>' https://memories.apexpots.com/explore/stats.json
```

### GET /explore/holders.json

List memory holders. Auth: bearer token if `ops_token_required=true`.

- **Query params:** `q?` (ilike match), `limit?` (default 100, clamped 1–1000).
- **Response:** `{count, holders: [{holder, records, sessions, first_seen, last_seen}]}`, ordered by `last_seen` DESC.

```bash
curl -H 'Authorization: Bearer <TOKEN>' \
  'https://memories.apexpots.com/explore/holders.json?limit=10'
```

### GET /explore/sessions.json

List sessions under a holder. Auth: bearer token if `ops_token_required=true`.

- **Query params:** `holder` (required), `limit?` (default 100, clamped 1–1000).
- **Response:** `{holder, count, sessions: [{session, module_iri, records, first_seen, last_seen}]}`. Session `(no session)` for null session IRIs.

```bash
curl -H 'Authorization: Bearer <TOKEN>' \
  'https://memories.apexpots.com/explore/sessions.json?holder=agent%3Amy-bot'
```

### GET /explore/records.json

List records under a holder/session. Auth: bearer token if `ops_token_required=true`.

- **Query params:** `holder` (required), `session?`, `module?`, `limit?` (default 100, clamped 1–1000).
- **Response:** `{holder, count, records: [{record_id, record_iri, module_iri, session_iri?, holder_iri?, root_statement?, root_context?, created_at, metadata}]}`, ordered by `created_at` DESC.

```bash
curl -H 'Authorization: Bearer <TOKEN>' \
  'https://memories.apexpots.com/explore/records.json?holder=agent%3Amy-bot&session=discord%3Auser%3A12345'
```

### GET /explore/facts.json

List facts (statements) derived from a record or session. Auth: bearer token if `ops_token_required=true`.

- **Query params:** `record_iri?` (`ctx:memory/claim/<uuid>` or `ctx:memory/episodic/<uuid>`), `session?`, `holder?` (required when `session` is set), `limit?` (default 100, clamped 1–2000).
- **Response:** `{count, facts: [{statement_id, subject, predicate, object_iri, object_lit, context, flags, polarity, tx_lo}]}`. Either `record_iri` or `session` is required; `holder` is required when `session` is set (cross-holder isolation). `flags` low 2 bits encode polarity.

```bash
curl -H 'Authorization: Bearer <TOKEN>' \
  'https://memories.apexpots.com/explore/facts.json?record_iri=ctx%3Amemory%2Fepisodic%2F550e8400-e29b-41d4-a716-446655440000'
```

### GET /openapi.json

OpenAPI 3.0 specification. Auth: none. Returns the full JSON spec (cached at startup).

```bash
curl https://memories.apexpots.com/openapi.json | jq .
```

### Documentation surfaces (auth: none)

- `GET /docs` — Swagger UI (HTML), loads `/openapi.json`.
- `GET /agent.md` — integration guide for AI agents (text/markdown).
- `GET /llms.txt` — same content at the canonical `llms.txt` path (text/plain).
- `GET /integration-patterns.md` — integration patterns and recipes (text/markdown).
- `GET /` — homepage (HTML, with ETag / Cache-Control).

```bash
curl https://memories.apexpots.com/agent.md
```

---

## donto-embed-coordinator

`https://donto.org/embed` (also `https://memories.apexpots.com/embed`) — a distributed embedding work queue that offloads CPU-bound `bge-small` embedding to spare machines and GPUs. A worker is dumb: **lease → embed (`BAAI/bge-small-en-v1.5`, 384 dims) → submit**; it never touches the database directly. The queue uses `FOR UPDATE SKIP LOCKED` for concurrent-safe distribution, with ~15-minute stale-lease reclaim. Three providers ship: `memory_chunk`, `predicate`, `entity` — all 384-dim.

Every endpoint requires `Authorization: Bearer <TOKEN>` **except** `/embed/health`. The model is pinned; do not change it without re-embedding the entire fabric. No OpenAPI spec is exposed. Remember the Cloudflare UA gotcha — add `-A 'curl/8'`.

### GET /embed/health

Health check with provider list. Auth: none.

- **Request:** no body.
- **Response:** `{ok: boolean, targets: string[]}` — always `{"ok": true, "targets": ["memory_chunk", "predicate", "entity"]}`.

```bash
curl -A 'curl/8' https://donto.org/embed/health
```

### GET /embed/stats

Embedding queue statistics and per-provider source/embedded counts. Auth: Bearer token.

- **Request:** no body / query params.
- **Response:** `{ "<provider>": {source: integer|null, embedded: integer|null, queue: {pending, leased, done}} }` for each of `memory_chunk`, `predicate`, `entity`. Source/embedded counts are cached (TTL 300s, configurable via `EMBED_COUNT_TTL_S`); queue counts are live. `401` if token missing/invalid.

```bash
curl -A 'curl/8' -H 'Authorization: Bearer <TOKEN>' https://donto.org/embed/stats
```

### POST /embed/enqueue

Enqueue pending items from a source into the embedding work queue. Auth: Bearer token.

- **Request body:** `{target: string (required: memory_chunk|predicate|entity), limit?: integer (default 50000)}`.
- **Response:** `{target, enqueued: integer}`. Scans the source table for items missing embeddings and inserts up to `limit` into `donto_embed_queue` with `status='pending'`. Idempotent (`ON CONFLICT (target,item_id) DO NOTHING`). `401` if token missing/invalid; `400` if target unknown.

```bash
curl -A 'curl/8' -X POST -H 'Authorization: Bearer <TOKEN>' -H 'Content-Type: application/json' \
  -d '{"target":"memory_chunk","limit":10000}' \
  https://donto.org/embed/enqueue
```

### POST /embed/lease

Lease embedding items for a worker to compute. Auth: Bearer token.

- **Request body:** `{worker_id?: string (default `anon`), n?: integer (default 128, capped 1024), target?: string (filter to one provider)}`.
- **Response:** `{batch: [{target, item_id, text, model, dim}]}`. `model` is `BAAI/bge-small-en-v1.5`, `dim` is 384. Atomically leases up to `n` pending items (or stale leases older than `EMBED_LEASE_TTL_MIN`=15min), setting `status='leased'`, `leased_by`, `leased_at`. Uses `FOR UPDATE SKIP LOCKED` to avoid double-booking; leases auto-reclaim if not submitted within the TTL.

```bash
curl -A 'curl/8' -X POST -H 'Authorization: Bearer <TOKEN>' -H 'Content-Type: application/json' \
  -d '{"worker_id":"worker-1","n":100,"target":"memory_chunk"}' \
  https://donto.org/embed/lease
```

### POST /embed/submit

Submit computed embeddings and mark the queue items done. Auth: Bearer token.

- **Request body:** `{items: [{target: string (memory_chunk|predicate|entity), item_id: string, vector: number[]}]}`. Vector length must match the provider's dim (384 for all current providers).
- **Response:** `{upserted: integer}`. Upserts vectors into the target table (`donto_x_memory_chunk_embedding`, `donto_predicate_embedding`, `donto_entity_embedding`), sets `status='done'`, records the text `sig_hash`. Wrong-dim vectors are logged and skipped. Transactional per target.

```bash
curl -A 'curl/8' -X POST -H 'Authorization: Bearer <TOKEN>' -H 'Content-Type: application/json' \
  -d '{"items":[{"target":"memory_chunk","item_id":"abc-123","vector":[0.1,0.2]}]}' \
  https://donto.org/embed/submit
```

---

## donto-api (genealogy extraction & ingest)

`https://genes.apexpots.com` — a FastAPI service that extracts knowledge from text via an LLM and ingests it into the knowledge graph, plus rich read/search/graph/alignment endpoints over the substrate (it talks to dontosrv internally). The graph is bitemporal and paraconsistent: facts carry valid-time and transaction-time, support historical as-of queries, and are soft-deleted via bitemporal closure. Predicates are open-world minted by the LLM; alignment solves vocabulary variance. Maturity levels 0–4 track quality. No auth (the operator may add bearer-token enforcement later). OpenAPI: `https://genes.apexpots.com/openapi.json` (authoritative for signatures).

### POST /extract-and-ingest

Extract knowledge from text and ingest into the graph (primary agent endpoint). Auth: none.

- **Request body:** `text` (string, required) — source document; `context` (string, required) — context IRI, e.g. `ctx:genes/mary-watson/obituary`; `model` (string, optional, default `glm`) — LLM shortcut (`glm`=GLM-5, `grok`, `mistral`) or full OpenRouter ID; `mode` (string, optional, default `opencode`) — `opencode` (GLM-5.1 agent) | `exhaustive` | `fast`; `budget_usd` (number, optional) — per-call USD cap.
- **Response:** `{model, context, mode, facts_extracted, statements_ingested, anchors_attached, yield (facts/char), elapsed_ms, timing: {...}, usage: {...}}`.
- **Notes:** synchronous; 10-minute timeout. ~60–150 facts per 500-word article. Idempotent (content-hash dedup).

```bash
curl -X POST https://genes.apexpots.com/extract-and-ingest \
  -H 'Content-Type: application/json' \
  -d '{"text": "Mary Watson was born in Cornwall in 1860. She married Robert Watson in Cooktown, Queensland in 1879.", "context": "ctx:genes/mary-watson/obituary"}'
```

### POST /extract

Extract knowledge with optional dry-run preview. Auth: none.

- **Request body:** `text` (required), `context` (required), `model?` (default `glm`), `mode?`, `dry_run?` (boolean, default `false`).
- **Response:** if `dry_run=false`: `{model, context, facts_extracted, statements_ingested, anchors_attached, yield, elapsed_ms, document}`. If `dry_run=true`: `{model, facts_extracted, yield, dry_run: true, facts: [{subject (ex:kebab-case), predicate (camelCase), object (iri or {v, dt}), tier (1-8), confidence (0.0-1.0), notes}], elapsed_ms}`. When `dry_run=false`, registers the source as `donto_document` + revision and writes `donto_span` + `donto_evidence_link` rows.

```bash
curl -X POST https://genes.apexpots.com/extract \
  -H 'Content-Type: application/json' \
  -d '{"text": "...", "context": "ctx:genes/test", "dry_run": true}'
```

### POST /jobs/extract

Submit an extraction job (async, returns immediately). Auth: none.

- **Request body:** `text` (required), `context` (required), `model?` (default `glm`), `mode?` (default `opencode`).
- **Response:** `{job_id: uuid}`. Durable via Temporal (survives restarts); duplicate submissions for the same context are rejected. Poll `GET /jobs/{job_id}`.

```bash
curl -X POST https://genes.apexpots.com/jobs/extract \
  -H 'Content-Type: application/json' \
  -d '{"text": "...", "context": "ctx:genes/mary-watson/async"}' | jq '.job_id'
```

### GET /jobs/{job_id}

Get job status and result. Auth: none.

- **Path param:** `job_id` (uuid).
- **Response:** `{job_id, status (queued|extracting|ingesting|completed|failed), result?: {facts_extracted, statements_ingested, anchors_attached, model, context, elapsed_ms}, error?}`.

```bash
curl https://genes.apexpots.com/jobs/{job_id} | jq '.status'
```

### GET /jobs/{job_id}/facts

Get extracted facts from a completed job. Auth: none.

- **Path param:** `job_id` (uuid). **Query:** `limit?` (default 200).
- **Response:** array of `{statement_id, subject, predicate, object_iri|object_lit, context, polarity, maturity, valid_lo, valid_hi, tx_lo}`. Use `statement_id` with `/claim/{id}` for evidence.

```bash
curl 'https://genes.apexpots.com/jobs/{job_id}/facts?limit=50'
```

### POST /assert

Assert a single statement into the graph. Auth: none.

- **Request body:** `subject` (required, `ex:kebab-case`), `predicate` (required, `camelCase`), `object_iri?` (mutually exclusive with `object_lit`), `object_lit?` (`{v, dt, lang?}`, mutually exclusive with `object_iri`), `context?` (default `donto:anonymous`), `polarity?` (default `asserted`: `asserted|negated|absent|unknown`), `maturity?` (default 0; 0–4).
- **Response:** `{statement_id: uuid, existing: boolean}`. Idempotent (content-hash dedup).

```bash
curl -X POST https://genes.apexpots.com/assert \
  -H 'Content-Type: application/json' \
  -d '{"subject": "ex:mary-watson", "predicate": "bornIn", "object_iri": "ex:cornwall-england", "context": "ctx:genes/mary-watson/manual", "maturity": 3}'
```

### POST /assert/batch

Assert multiple statements atomically. Auth: none.

- **Request body:** an array of `/assert` statement objects.
- **Response:** `{inserted, skipped, total}`. Single transaction (all-or-none); per-statement dedup.

```bash
curl -X POST https://genes.apexpots.com/assert/batch \
  -H 'Content-Type: application/json' \
  -d '[{"subject": "ex:mary-watson", "predicate": "marriedTo", "object_iri": "ex:robert-watson"}, {"subject": "ex:mary-watson", "predicate": "bornOn", "object_lit": {"v": "1860-01-15", "dt": "xsd:date"}}]'
```

### GET /search

Full-text search by name/label (trigram-indexed, ~5ms). Auth: none.

- **Query params:** `q` (required) — trigram-matched, case-insensitive, partial; `limit?` (default 25; 1–100).
- **Response:** `{matches: [{subject (iri), label, count (fact count)}], q}`. Ordered by fact count (most-connected first). The fastest way to find entities.

```bash
curl 'https://genes.apexpots.com/search?q=mary+watson&limit=10'
```

### GET /history/{subject}

Get all facts about an entity (complete statement history). Auth: none.

- **Path param:** `subject` (entity IRI). **Query:** `limit?`.
- **Response:** `{count, rows: [{statement_id, predicate, object_iri|object_lit, context, polarity, maturity, valid_lo, valid_hi, tx_lo, tx_hi}]}`. Includes retracted statements (`tx_hi` set).

```bash
curl 'https://genes.apexpots.com/history/ex:mary-watson?limit=50'
```

### GET /statement/{id}

Get a single statement by UUID. Auth: none.

- **Path param:** `id` (uuid).
- **Response:** `{statement_id, subject, predicate, object_iri|object_lit, context, polarity, maturity, valid_lo, valid_hi, tx_lo, tx_hi}`.

```bash
curl 'https://genes.apexpots.com/statement/{statement_uuid}'
```

### POST /retract/{statement_id}

Retract a statement (bitemporal soft-delete). Auth: none.

- **Path param:** `statement_id` (uuid).
- **Response:** `{retracted: boolean, statement_id}`. Sets `tx_hi`; data not destroyed (as-of queries before retraction still return it). Idempotent.

```bash
curl -X POST 'https://genes.apexpots.com/retract/{statement_uuid}'
```

### GET /subjects

List top subjects by fact count. Auth: none.

- **Request:** none.
- **Response:** `{subjects: [{subject (iri), count}]}` — top 50 by count descending.

```bash
curl 'https://genes.apexpots.com/subjects'
```

### GET /contexts

List all contexts (scopes/namespaces). Auth: none.

- **Request:** none.
- **Response:** `{contexts: [{context (iri), kind, mode, count}]}`. Formats: `ctx:genes/<topic>/<source>`, `ctx:extract/<file>/<model>`, `ctx:genealogy/research-db`.

```bash
curl 'https://genes.apexpots.com/contexts'
```

### GET /predicates

List all predicates with usage counts. Auth: none.

> ⚠️ **Shadowed on the public host.** At `https://genes.apexpots.com/predicates` this path is intercepted by the dontopedia web app and returns **HTML**, not API JSON (verified 2026-06-07). To get the JSON, call it on the internal port (`http://localhost:7879/predicates` via dontosrv) or use `/viz/predicates`, or query the substrate directly. Sibling paths (`/subjects`, `/contexts`, `/search`, `/version`) are *not* shadowed and correctly return JSON on `genes.apexpots.com`.

- **Request:** none.
- **Response:** `{predicates: [{predicate, count}]}` by count descending. (Top: `rdf:type` ~3.9M, `donto:status` ~1.6M, `donto:textSpan` ~1.2M, `ex:knownAs` ~1M, `rdfs:label` ~589K.)

```bash
curl 'http://localhost:7879/predicates'   # genes.apexpots.com/predicates returns the web app's HTML
```

### POST /query

Run a DontoQL or SPARQL query. Auth: none.

- **Request body:** `{query: string}` — DontoQL (`MATCH ...`) or SPARQL (`SELECT`/`PREFIX ...`). E.g. `MATCH ?s ?p ?o LIMIT 20`.
- **Response:** array of result rows, each a JSON object of bound variables. Cost 1–10s depending on complexity.

```bash
curl -X POST 'https://genes.apexpots.com/query' \
  -H 'Content-Type: application/json' \
  -d '{"query": "MATCH ?s bornIn ?place LIMIT 10"}'
```

### GET /connections/{entity}

Get all bidirectional connections (incoming + outgoing edges). Auth: none.

- **Path param:** `entity` (iri). **Query:** `limit?` (default 500), `context?`, `min_maturity?` (default 0; 0–4).
- **Response:** `{outgoing: [{predicate, object_iri|object_lit, context, maturity}], incoming: [{predicate, subject, context, maturity}], entity}`.

```bash
curl 'https://genes.apexpots.com/connections/ex:mary-watson?limit=100&min_maturity=2'
```

### POST /graph/neighborhood

Get a neighborhood subgraph for visualization. Auth: none.

- **Request body:** `subject` (required, center IRI), `depth?` (default 1, max 3), `predicates?` (string[]), `context?`, `min_maturity?` (default 0; 0–4), `limit?` (default 500, max 5000).
- **Response:** `{nodes: [{id (iri), label, type, degree}], edges: [{source, target, predicate, context, maturity}], center, depth}` (D3-ready).

```bash
curl -X POST 'https://genes.apexpots.com/graph/neighborhood' \
  -H 'Content-Type: application/json' \
  -d '{"subject": "ex:mary-watson", "depth": 2, "predicates": ["marriedTo", "childOf", "parentOf"]}'
```

### POST /graph/path

Find the shortest path between two entities. Auth: none.

- **Request body:** `source` (required IRI), `target` (required IRI), `max_depth?` (default 6), `predicates?` (string[]).
- **Response:** `{paths: [[{subject, predicate, object}, ...]], found: boolean, depth}`.

```bash
curl -X POST 'https://genes.apexpots.com/graph/path' \
  -H 'Content-Type: application/json' \
  -d '{"source": "ex:mary-watson", "target": "ex:cooktown-municipality"}'
```

### POST /graph/subgraph

Get all edges matching given predicates (themed visualization). Auth: none.

- **Request body:** `predicates` (required string[]), `context?`. E.g. family=`["childOf","parentOf","marriedTo","siblingOf"]`.
- **Response:** `{nodes: [{id, label, type, degree}], edges: [{source, target, predicate, context, maturity}], node_count, edge_count}` (D3-ready).

```bash
curl -X POST 'https://genes.apexpots.com/graph/subgraph' \
  -H 'Content-Type: application/json' \
  -d '{"predicates": ["bornIn", "diedIn", "marriedTo", "childOf"]}'
```

### POST /align/register

Register a predicate alignment. Auth: none.

- **Request body:** `source` (required predicate), `target` (required predicate), `relation` (required: `exact_equivalent|inverse_equivalent|sub_property_of|close_match|not_equivalent`), `confidence?` (default 1.0; 0.0–1.0).
- **Response:** `{alignment_id: uuid}`. Must call `POST /align/rebuild` afterward for it to take effect.

```bash
curl -X POST 'https://genes.apexpots.com/align/register' \
  -H 'Content-Type: application/json' \
  -d '{"source": "bornInPlace", "target": "bornIn", "relation": "exact_equivalent", "confidence": 0.95}'
```

### POST /align/rebuild

Rebuild the predicate closure index. Auth: none.

- **Request:** none.
- **Response:** `{rows: integer}` (closure table size, ~12K). Call after any `/align/register`. Cost 1–5s.

```bash
curl -X POST 'https://genes.apexpots.com/align/rebuild'
```

### GET /align/suggest/{predicate}

Find similar predicates (trigram similarity). Auth: none.

- **Path param:** `predicate`. **Query:** `threshold?` (default 0.3; 0.0–1.0), `limit?` (default 20).
- **Response:** `[{source, target, similarity, label}]` by similarity descending.

```bash
curl 'https://genes.apexpots.com/align/suggest/bornInPlace?threshold=0.6&limit=10'
```

### GET /evidence/{statement_id}

Get evidence spans linked to a statement. Auth: none.

- **Path param:** `statement_id` (uuid).
- **Response:** `{evidence: [{link_type (produced_by|extracted_from|mentioned_in|supports|refutes|contextualizes), document (iri), span, surface_text, confidence (0-1)}]}`. Empty if inserted via `/assert`.

```bash
curl 'https://genes.apexpots.com/evidence/{statement_uuid}'
```

### GET /claim/{statement_id}

Full claim card (statement + evidence + arguments + obligations). Auth: none.

- **Path param:** `statement_id` (uuid).
- **Response:** `{subject, predicate, object_iri|object_lit, context, polarity, maturity, asserted_at, evidence: [...], arguments: [{type (supports|refutes|undercuts|qualifies), statement}], obligations: [...], blockers: [...]}`.

```bash
curl 'https://genes.apexpots.com/claim/{statement_uuid}'
```

### GET /health

Health check (verifies API + dontosrv). Auth: none.

- **Response:** `{status (ok|degraded|down), dontosrv (ok|error), agent_instructions?}`.

```bash
curl 'https://genes.apexpots.com/health'
```

### GET /version

Version info. Auth: none.

- **Response:** `{dir, service (dontosrv), version, agent_instructions?}`.

```bash
curl 'https://genes.apexpots.com/version'
```

### GET /firehose/stream

Real-time SSE stream of database activity. Auth: none.

- **Request:** none (Server-Sent Events).
- **Response:** SSE stream of `{audit_id, at, actor, action (insert|retract|...), statement_id, detail}`. Backed by Postgres `LISTEN/NOTIFY` on `donto_firehose` / `donto_audit`.

```bash
curl 'https://genes.apexpots.com/firehose/stream' 2>/dev/null | head -20
```

### GET /firehose/recent

Recent audit events (last N). Auth: none.

- **Query:** `limit?` (default 100).
- **Response:** `[{audit_id, at, actor, action, statement_id, detail}]`.

```bash
curl 'https://genes.apexpots.com/firehose/recent?limit=20'
```

### GET /jobs

List all extraction jobs with status. Auth: none.

- **Query:** `status?` (`queued|extracting|ingesting|completed|failed`).
- **Response:** `{jobs: [{job_id, status, text_preview, context, model, created_at, updated_at, result?}]}`.

```bash
curl 'https://genes.apexpots.com/jobs?status=completed'
```

---

## dontosrv (core substrate)

The core bitemporal claim substrate HTTP sidecar — ~70 endpoints covering the claim write-path, rich read/search, evidence linking, the Trust Kernel, obligations/reactions, predicate alignment, shape validation & rule derivation, and schema discovery. It is reachable internally at `localhost:7879`; the public genealogy surface (`https://genes.apexpots.com`) proxies a curated subset. All endpoints return JSON unless noted. Reads are open; writes are not gated at the HTTP layer (policy gates apply at the SQL layer per context/action). Examples below use the internal `http://localhost:7879` base; substitute your deployment's reachable host.

> Claims are bitemporal (`tx_time`/`valid_time`) with polarity (`asserted/negated/absent/unknown`) and maturity (E0–E4). When `DONTO_ENFORCE_CLAIM_INVARIANT=true`, anchorless/unflagged claims land at maturity E1 (never rejected, just un-promoted).

### Health & metrics

#### GET /health

Service health check. Auth: none. Returns `text/plain: ok`.

```bash
curl http://localhost:7879/health
```

#### GET /version

Service version. Auth: none. Returns `{service, version, dir}`.

```bash
curl http://localhost:7879/version
```

#### GET /metrics

Prometheus-format substrate health metrics. Auth: none. Returns `text/plain` gauges (`donto_up`, `donto_currently_believed_statements`, `donto_distinct_subjects`, `donto_distinct_predicates`, `donto_distinct_contexts`, `donto_evidence_links`, `donto_contested_subject_predicate_pairs`, `donto_total_contradiction_pressure`, `donto_consumer_namespaces`, `donto_registered_overlays`, `donto_identity_hypotheses`, `donto_argument_edges`, `donto_anchor_coverage_pct`, `donto_statements_estimated`, …). Same numeric fields as the `donto_v_substrate_health` view.

```bash
curl http://localhost:7879/metrics | grep donto_currently_believed_statements
```

### Claim write-path

#### POST /assert

Assert one bitemporal claim. Auth: none.

- **Request body:** `{subject, predicate, object_iri?, object_lit?: {v, dt, lang?}, context?='donto:anonymous', polarity?='asserted', maturity?=0, valid_from?: date, valid_to?: date, anchor?: {revision_id: uuid, start: int32, end: int32, surface_text: string, link_type?='extracted_from'}, hypothesis_only?=false}`.
- **Response:** `{statement_id: uuid}` or `{error}`. When `anchor` is provided, materializes `donto_span` + `donto_evidence_link`.

```bash
curl -X POST http://localhost:7879/assert -H 'Content-Type: application/json' \
  -d '{"subject":"ex:person1","predicate":"ex:motherOf","object_iri":"ex:person2","context":"donto:evidence","maturity":2}'
```

#### POST /assert/batch

Assert many claims in one call. Auth: none.

- **Request body:** `{statements: [StatementInput]}` (each like `/assert`).
- **Response:** `{inserted: int64, anchors_attached: int64}` or `{error}`.

```bash
curl -X POST http://localhost:7879/assert/batch -H 'Content-Type: application/json' \
  -d '{"statements":[{"subject":"ex:a","predicate":"ex:p","object_iri":"ex:b"},{"subject":"ex:c","predicate":"ex:q","object_iri":"ex:d"}]}'
```

#### POST /retract

Retract a claim (close its `tx_time` interval). Auth: none.

- **Request body:** `{statement_id: uuid}`.
- **Response:** `{statement_id, retracted: boolean}`. Retracted rows stay in `donto_statement` with closed intervals.

```bash
curl -X POST http://localhost:7879/retract -H 'Content-Type: application/json' \
  -d '{"statement_id":"49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae"}'
```

#### POST /contexts/ensure

Idempotently create/ensure a context. Auth: none.

- **Request body:** `{iri, kind?='source', mode?='permissive', parent?}`. `kind` ∈ `source|hypothesis|derived|snapshot|user|…`; `mode` ∈ `permissive|strict|…`.
- **Response:** `{iri, ok: boolean}` or `{error}`.

```bash
curl -X POST http://localhost:7879/contexts/ensure -H 'Content-Type: application/json' \
  -d '{"iri":"donto:genes-evidence","kind":"source","mode":"permissive"}'
```

### Read & search

#### GET /search

Label/IRI/literal substring search; distinct subjects ranked by live-row count. Auth: none.

- **Query:** `q` (required), `limit?` (default 25, max 100).
- **Response:** `{q, matches: [{subject, label|null, count}]}`.

```bash
curl 'http://localhost:7879/search?q=person&limit=10'
```

#### GET /history/{subject}

All statements about a subject (open + closed, all contexts/polarities). Auth: none.

- **Path:** `subject` (IRI). **Query:** `limit?` (default 2000, max 20000), `context?`, `predicate?`, `from?` (date), `to?` (date), `include_retracted?` (default true).
- **Response:** `{subject, total, limit, statements: [{statement_id, subject, predicate, object_iri, object_lit, context, polarity, maturity, valid_lo, valid_hi, tx_lo, tx_hi, lineage: [uuid]}]}`.

```bash
curl 'http://localhost:7879/history/ex:person1?limit=100&context=donto:evidence'
```

#### GET /statement/{id}

Full statement detail: row, lineage, audit log, certificate, siblings. Auth: none.

- **Path:** `id` (uuid).
- **Response:** `{statement: StatementRow, lineage: {sources: [...], derived: [...]}, audit: [...], certificate: {}|null, siblings: [...]}`.

```bash
curl 'http://localhost:7879/statement/49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae'
```

#### GET /claim/{id}

Claim card (`donto_claim_card`): the claim + evidence + contradiction context. Auth: none.

- **Path:** `id` (uuid).
- **Response:** free-form JSON projected by the `donto_claim_card(uuid)` SQL function (shape varies by claim topology).

```bash
curl 'http://localhost:7879/claim/49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae'
```

#### POST /recall

Policy-gated recall projection (`donto_recall_projection`): identity-resolved, bitemporally-sliced rows with per-row effective actions. Auth: none.

- **Request body:** `{holder, action ('read_metadata'|'read_content'|'quote'|'derive_claims'|…), subject?, predicate?, object_iri?, scope?: {root, depth?}, polarity?='asserted', min_maturity?=0, as_of_tx?: datetime, as_of_valid?: date, lens_name?, limit?=200, permitted_only?=false}`.
- **Response:** `{holder, action, lens|null, as_of|null, row_count, rows: [StatementRow + {resolved_subject|null, resolved_object|null, effective_actions: {}, action_allowed: boolean}]}`.

```bash
curl -X POST http://localhost:7879/recall -H 'Content-Type: application/json' \
  -d '{"holder":"alice@genes.apexpots.com","action":"read_content","limit":50}'
```

#### GET /subjects

List subjects with live statement count (limit 5000). Auth: none. Returns `{subjects: [{subject, count}]}` (live counts, descending).

```bash
curl 'http://localhost:7879/subjects'
```

#### GET /subjects/all

List all subjects including retracted-only ones. Auth: none. Returns `{subjects: [{subject, count}]}`.

```bash
curl 'http://localhost:7879/subjects/all'
```

#### POST /subjects/refresh

Refresh the subject-count materialized view (`donto_subject_count`). Auth: none. Returns `{ok: boolean}`.

```bash
curl -X POST http://localhost:7879/subjects/refresh
```

#### GET /inbound/{subject}

Statements where the subject is the object (inverse view). Auth: none. Returns `{subject, inbound: [StatementRow]}`.

```bash
curl 'http://localhost:7879/inbound/ex:person1'
```

#### GET /cluster/{subject}

Subject's identity cluster (identity-resolved neighbours). Auth: none. Returns `{subject, cluster: [{resolved_id, confidence}]}`.

```bash
curl 'http://localhost:7879/cluster/ex:person1'
```

#### GET /context-facts

Distinct `(subject, predicate, object)` triple count per context. Auth: none. Returns `{contexts: [{context, fact_count}]}`.

```bash
curl 'http://localhost:7879/context-facts'
```

#### GET /context-spans

Context-scoped evidence span counts. Auth: none. Returns `{contexts: [{context, span_count}]}`.

```bash
curl 'http://localhost:7879/context-spans'
```

### Identity & predicates

#### POST /subjects/merge

Merge two subjects (identity binding). Auth: none. Body `{source, target}` → `{source, target, ok}`.

```bash
curl -X POST http://localhost:7879/subjects/merge -H 'Content-Type: application/json' \
  -d '{"source":"ex:person-a","target":"ex:person-b"}'
```

#### GET /predicates/align-suggest

Suggest predicate alignments (cross-vocabulary candidates). Auth: none. **Query:** `limit?` (default 100). Returns `{suggestions: [{source, target, confidence}]}`.

```bash
curl 'http://localhost:7879/predicates/align-suggest?limit=50'
```

#### POST /predicates/merge

Merge two predicates (canonical registration). Auth: none. Body `{source, target}` → `{source, target, ok}`.

```bash
curl -X POST http://localhost:7879/predicates/merge -H 'Content-Type: application/json' \
  -d '{"source":"ex:parentOf","target":"ex:parent_of"}'
```

#### GET /predicates/fragmentation

Predicate fragmentation report (variant clustering). Auth: none. Returns `{predicates: [{canonical, variant_count, variants: [string]}]}`.

```bash
curl 'http://localhost:7879/predicates/fragmentation'
```

#### GET /predicates/suggest

Suggest predicates by substring/similarity. Auth: none. **Query:** `q`, `limit?` (default 25). Returns `{q, predicates: [{iri, label|null, count}]}`.

```bash
curl 'http://localhost:7879/predicates/suggest?q=mother&limit=10'
```

#### GET /entities/suggest

Suggest entities (subjects) by substring/similarity. Auth: none. **Query:** `q`, `limit?` (default 25). Returns `{q, entities: [{subject, label|null, count}]}`.

```bash
curl 'http://localhost:7879/entities/suggest?q=john&limit=10'
```

#### POST /predicates/refresh

Refresh the predicate-count view (`donto_predicate_count`). Auth: none. Returns `{ok}`.

```bash
curl -X POST http://localhost:7879/predicates/refresh
```

#### GET /predicates

List predicates by usage descending (limit 500). Auth: none. Returns `{predicates: [{predicate, count}]}`.

```bash
curl 'http://localhost:7879/predicates'
```

#### GET /contexts

List contexts (limit 500). Auth: none. Returns `{contexts: [{context, kind, mode, count}]}`.

```bash
curl 'http://localhost:7879/contexts'
```

#### POST /contexts/lookup

Targeted metadata lookup for known context IRIs (faster than `/contexts` on hot paths). Auth: none. Body `{iris: [string]}` → `{contexts: [{context, kind, mode, count}]}`.

```bash
curl -X POST http://localhost:7879/contexts/lookup -H 'Content-Type: application/json' \
  -d '{"iris":["donto:evidence","donto:hypothesis"]}'
```

### Documents & evidence

#### POST /sources/lookup

Metadata for registered document sources. Auth: none. Body `{iris: [string]}` → `{sources: [{document_id, document_iri, label|null, source_url|null, source_kind|null, media_type, revision_id|null, has_body}]}`.

```bash
curl -X POST http://localhost:7879/sources/lookup -H 'Content-Type: application/json' \
  -d '{"iris":["donto:genes-paper-1"]}'
```

#### POST /statements/evidence

Evidence (spans + links) for statements. Auth: none. Body `{statement_ids: [uuid]}` → `{evidence: {<statement_id>: [{link_type, target_document_id|null, target_span_id|null, confidence|null}]}}`.

```bash
curl -X POST http://localhost:7879/statements/evidence -H 'Content-Type: application/json' \
  -d '{"statement_ids":["49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae"]}'
```

#### POST /documents/register

Register a document source (legacy; no required policy). Auth: none. Body `{iri, media_type?='text/plain', label?, source_url?, language?, policy_iri?}` → `{document_id, iri, policy_iri|null, warning?}`. Deprecated in favour of `/sources/register`.

```bash
curl -X POST http://localhost:7879/documents/register -H 'Content-Type: application/json' \
  -d '{"iri":"donto:genes-paper-2026","media_type":"text/html"}'
```

#### POST /sources/register

Register a document source with a required policy. Auth: none. Body `{iri, source_kind, policy_iri, media_type?, label?, source_url?, language?}` → `{document_id, iri, policy_iri}`. No source without policy.

```bash
curl -X POST http://localhost:7879/sources/register -H 'Content-Type: application/json' \
  -d '{"iri":"donto:genes-paper-2026","source_kind":"academic-paper","policy_iri":"donto:policy-public-read"}'
```

#### POST /documents/revision

Add a revision (version) of a document body. Auth: none. Body `{document_id, body?, parser_version?}` → `{revision_id}`.

```bash
curl -X POST http://localhost:7879/documents/revision -H 'Content-Type: application/json' \
  -d '{"document_id":"...","body":"The full document text here..."}'
```

#### GET /documents/revision/{revision_id}/body

Retrieve the full body text of a revision. Auth: none. Returns `{revision_id, body, byte_size}` or `404` if the revision has no text body.

```bash
curl 'http://localhost:7879/documents/revision/49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae/body'
```

#### POST /evidence/link/span

Link a statement to an evidence span. Auth: none. Body `{statement_id, span_id, link_type?='extracted_from', confidence?, context?}` → `{link_id}`.

```bash
curl -X POST http://localhost:7879/evidence/link/span -H 'Content-Type: application/json' \
  -d '{"statement_id":"49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae","span_id":"..."}'
```

#### GET /evidence/{stmt}

All evidence for a statement (`donto_evidence_for`). Auth: none. **Path:** `stmt` (uuid). Returns `{evidence: [{link_id, link_type, target_document_id|null, target_revision_id|null, target_span_id|null, target_annotation_id|null, target_run_id|null, target_statement_id|null, confidence|null}]}`.

```bash
curl 'http://localhost:7879/evidence/49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae'
```

### Arguments & reactions

#### POST /arguments/assert

Assert a typed argument edge between two statements. Auth: none. Body `{source: uuid, target: uuid, relation ('supports'|'rebuts'|'undercuts'|'endorses'|'supersedes'|'qualifies'|'potentially_same'|'same_referent'|'same_event'), context?='donto:anonymous', strength?, agent_id?, evidence?}` → `{argument_id}`.

```bash
curl -X POST http://localhost:7879/arguments/assert -H 'Content-Type: application/json' \
  -d '{"source":"...","target":"...","relation":"supports","strength":0.8}'
```

#### GET /arguments/{stmt}

All argument edges touching a statement (`donto_arguments_for`). Auth: none. Returns `{arguments: [{argument_id, source, target, relation, strength|null, context}]}`.

```bash
curl 'http://localhost:7879/arguments/49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae'
```

#### GET /arguments/frontier

Contradiction frontier: statements ranked by net attack/support pressure. Auth: none. Returns `{frontier: [{statement_id, attack_count, support_count, net_pressure}]}` (`net_pressure = support_count - attack_count`).

```bash
curl 'http://localhost:7879/arguments/frontier'
```

#### POST /react

Attach a reaction to a statement (endorse/reject/cite/supersede). Auth: none. Body `{source: uuid, kind ('endorses'|'rejects'|'cites'|'supersedes'), object_iri?, context?='donto:anonymous', actor?}` → `{reaction_id}`.

```bash
curl -X POST http://localhost:7879/react -H 'Content-Type: application/json' \
  -d '{"source":"49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae","kind":"endorses"}'
```

#### GET /reactions/{id}

Enumerate current reactions to a statement. Auth: none. Returns `{reactions: [{reaction_id, kind, object_iri|null, context, polarity}], counts: {endorses, rejects, cites, supersedes}}`.

```bash
curl 'http://localhost:7879/reactions/49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae'
```

### Trust kernel (policy, authorisation, attestations, agents)

#### POST /policy/assign

Assign a policy to a target. Auth: none. Body `{target_kind ('context'|'document'|'overlay'|…), target_id, policy_iri, assigned_by?='system'}` → `{assignment_id}`.

```bash
curl -X POST http://localhost:7879/policy/assign -H 'Content-Type: application/json' \
  -d '{"target_kind":"context","target_id":"donto:evidence","policy_iri":"donto:policy-public-read"}'
```

#### GET /policy/effective/{target_kind}/{target_id}

Effective actions the caller may take on a target. Auth: none (caller from headers if present). Returns `{target_kind, target_id, caller, actions: {<action>: boolean}}`.

```bash
curl 'http://localhost:7879/policy/effective/context/donto:evidence'
```

#### POST /authorise

Authorisation probe (check without executing). Auth: none. Body `{target_kind, target_id, action}` → `{caller, target_kind, target_id, action, allowed: boolean}`.

```bash
curl -X POST http://localhost:7879/authorise -H 'Content-Type: application/json' \
  -d '{"target_kind":"context","target_id":"donto:evidence","action":"read_content"}'
```

#### POST /protected/read

Policy-gated read: execute a SQL select only if the caller's action is allowed. Auth: none. Body `{target_kind, target_id, action, query}` → `{rows: [...]}` or `{error, reason}`. (Input not sanitised — SQL-injection risk; operator-internal.)

```bash
curl -X POST http://localhost:7879/protected/read -H 'Content-Type: application/json' \
  -d '{"target_kind":"context","target_id":"donto:evidence","action":"read_content","query":"SELECT * FROM ..."}'
```

#### POST /attestations

Issue an attestation (Trust Kernel): holder may perform actions under a policy within time/scope. Auth: none. Body `{holder, issuer, policy_iri, actions: [string], purpose, rationale, expires_at?}` → `{attestation_id}`.

```bash
curl -X POST http://localhost:7879/attestations -H 'Content-Type: application/json' \
  -d '{"holder":"alice@genes","issuer":"admin","policy_iri":"donto:policy-genes-read","actions":["read_content"],"purpose":"research","rationale":"approved"}'
```

#### POST /attestations/{id}/revoke

Revoke an attestation. Auth: none. **Path:** `id` (uuid). Body `{revoked_by?='system', reason?}` → `{revoked: boolean, attestation_id}`.

```bash
curl -X POST http://localhost:7879/attestations/49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae/revoke \
  -H 'Content-Type: application/json' -d '{"revoked_by":"admin","reason":"policy change"}'
```

#### POST /agents/register

Register an agent (LLM, person, bot, system). Auth: none. Body `{iri, agent_type?='custom', label?, model_id?}` → `{agent_id, iri}`.

```bash
curl -X POST http://localhost:7879/agents/register -H 'Content-Type: application/json' \
  -d '{"iri":"donto:claude-opus","agent_type":"llm","model_id":"claude-opus-4"}'
```

#### POST /agents/bind

Bind an agent to a context (ownership/stewardship). Auth: none. Body `{agent_id, context, role?='owner'}` → `{ok}`.

```bash
curl -X POST http://localhost:7879/agents/bind -H 'Content-Type: application/json' \
  -d '{"agent_id":"...","context":"donto:genes-evidence","role":"owner"}'
```

### Obligations

#### POST /obligations/emit

Emit an obligation (task, reminder, review request). Auth: none. Body `{statement_id, obligation_type, context?='donto:anonymous', priority?=0, detail?, assigned_agent?}` → `{obligation_id}`.

```bash
curl -X POST http://localhost:7879/obligations/emit -H 'Content-Type: application/json' \
  -d '{"statement_id":"...","obligation_type":"review-needed","priority":1}'
```

#### POST /obligations/resolve

Resolve an obligation. Auth: none. Body `{obligation_id, resolved_by?, status?='resolved'}` → `{resolved: boolean}`.

```bash
curl -X POST http://localhost:7879/obligations/resolve -H 'Content-Type: application/json' \
  -d '{"obligation_id":"..."}'
```

#### POST /obligations/open

List open obligations (filterable). Auth: none. Body `{obligation_type?, context?, limit?=100}` → `{obligations: [{obligation_id, statement_id|null, obligation_type, priority, context, assigned_agent|null}]}`.

```bash
curl -X POST http://localhost:7879/obligations/open -H 'Content-Type: application/json' \
  -d '{"obligation_type":"review-needed","limit":50}'
```

#### GET /obligations/summary

Obligation summary (counts by type + status). Auth: none. Returns `{summary: [{obligation_type, status, count}]}`.

```bash
curl 'http://localhost:7879/obligations/summary'
```

### Shapes, rules, certificates

#### POST /shapes/validate

Validate claims against a shape (constraint) schema. Auth: none. Body `{shape_iri, scope: {} (ContextScope)}` → `{shape_iri, focus_count, violations: [{focus, reason, evidence: [uuid]}], source ('builtin'|'cached'|'lean')}`. Built-in shapes: `builtin:functional/<pred>`, `builtin:datatype/<pred>/<dt>`, `builtin:range/*`.

```bash
curl -X POST http://localhost:7879/shapes/validate -H 'Content-Type: application/json' \
  -d '{"shape_iri":"builtin:functional/ex:motherOf","scope":{"root":"donto:evidence"}}'
```

#### POST /rules/derive

Run a derivation rule and emit derived statements. Auth: none. Body `{rule_iri, scope: {} (ContextScope), into: string}` → `{rule_iri, into, emitted, source}`. Built-in rules: `builtin:transitive/<pred>`, `builtin:inverse/<pred>/<inv>`, `builtin:symmetric/<pred>`. Idempotent.

```bash
curl -X POST http://localhost:7879/rules/derive -H 'Content-Type: application/json' \
  -d '{"rule_iri":"builtin:transitive/ex:ancestorOf","scope":{"root":"donto:evidence"},"into":"donto:derived"}'
```

#### POST /certificates/attach

Attach a certificate to a statement (proof of derivation/justification). Auth: none. Body `{statement_id, kind ('direct_assertion'|'substitution'|'transitive_closure'|'confidence_justification'|'shape_entailment'|'hypothesis_scoped'|'replay'), rule_iri?, inputs?: [uuid], body: {}, signature?}` → `{statement_id, kind}`.

```bash
curl -X POST http://localhost:7879/certificates/attach -H 'Content-Type: application/json' \
  -d '{"statement_id":"...","kind":"transitive_closure","body":{"predicate":"ex:ancestorOf"}}'
```

#### POST /certificates/verify/{stmt}

Re-run certificate verification for a statement. Auth: none. **Path:** `stmt` (uuid). Returns `{statement_id, kind, ok: boolean, reason?}`.

```bash
curl -X POST http://localhost:7879/certificates/verify/49bbf329-e9d2-4c83-a56f-5c6c0aaf99ae
```

### Alignment & descriptors

#### POST /alignment/register

Register a predicate alignment. Auth: none. Body `{source, target, relation ('exact_equivalent'|'inverse_equivalent'|'sub_property_of'|'close_match'|'decomposition'|'not_equivalent'), confidence, valid_lo?, valid_hi?, run_id?, provenance?, actor?}` → `{alignment_id}`.

```bash
curl -X POST http://localhost:7879/alignment/register -H 'Content-Type: application/json' \
  -d '{"source":"ex:parentOf","target":"rdfs:parent_of","relation":"exact_equivalent","confidence":0.95}'
```

#### POST /alignment/retract

Retract a predicate alignment. Auth: none. Body `{alignment_id}` → `{alignment_id, retracted: boolean}`.

```bash
curl -X POST http://localhost:7879/alignment/retract -H 'Content-Type: application/json' \
  -d '{"alignment_id":"..."}'
```

#### POST /alignment/rebuild-closure

Rebuild the predicate closure (`donto_rebuild_predicate_closure()`). Auth: none. Returns `{rows: int64}`.

```bash
curl -X POST http://localhost:7879/alignment/rebuild-closure
```

#### POST /alignment/runs/start

Start an alignment run (batch matching/disambiguation). Auth: none. Body `{run_type, model_id?, config?, source_predicates?}` → `{run_id}`.

```bash
curl -X POST http://localhost:7879/alignment/runs/start -H 'Content-Type: application/json' \
  -d '{"run_type":"embedding-similarity","model_id":"sentence-transformers/all-MiniLM-L6-v2"}'
```

#### POST /alignment/runs/complete

Complete an alignment run. Auth: none. Body `{run_id, status, proposed?, accepted?, rejected?}` → `{run_id, ok: boolean}`.

```bash
curl -X POST http://localhost:7879/alignment/runs/complete -H 'Content-Type: application/json' \
  -d '{"run_id":"...","status":"success","proposed":150,"accepted":140,"rejected":10}'
```

#### POST /descriptors/upsert

Register/update a predicate descriptor. Auth: none. Body `{predicate_iri, gloss?, subject_type?, object_type?, domain?, example_subject?, example_object?}` → `{predicate_iri, ok: boolean}`.

```bash
curl -X POST http://localhost:7879/descriptors/upsert -H 'Content-Type: application/json' \
  -d '{"predicate_iri":"ex:motherOf","gloss":"X is the mother of Y","subject_type":"Person","object_type":"Person"}'
```

#### POST /descriptors/nearest

Find nearest predicates to a descriptor (similarity search). Auth: none. Body `{gloss, limit?=10}` → `{candidates: [{predicate, gloss, similarity}]}`.

```bash
curl -X POST http://localhost:7879/descriptors/nearest -H 'Content-Type: application/json' \
  -d '{"gloss":"X is parent of Y","limit":5}'
```

### Overlays (shadows)

#### POST /shadow/materialize

Materialize an overlay shadow (consumer-owned projection). Auth: none. Body `{overlay_iri, scope?, include_history?}` → `{overlay_iri, rows_materialized}`.

```bash
curl -X POST http://localhost:7879/shadow/materialize -H 'Content-Type: application/json' \
  -d '{"overlay_iri":"donto:genes-overlay","include_history":false}'
```

#### POST /shadow/rebuild

Rebuild all overlay shadows. Auth: none. Returns `{overlays_rebuilt: int64}`.

```bash
curl -X POST http://localhost:7879/shadow/rebuild
```

### Query languages

#### POST /sparql

SPARQL query execution. Auth: none. Body `{query, scope_preset?}` → `{rows: [...]}` or `{error}`. Subset: `PREFIX, SELECT, WHERE, GRAPH, FILTER, LIMIT, OFFSET`.

```bash
curl -X POST http://localhost:7879/sparql -H 'Content-Type: application/json' \
  -d '{"query":"SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 10"}'
```

#### POST /dontoql

DontoQL query execution (donto's native query language; richer than SPARQL — bitemporal scoping, policy gating, identity lensing, rules). Auth: none. Body `{query, scope_preset?}` → `{rows: [...]}` or `{error}`.

```bash
curl -X POST http://localhost:7879/dontoql -H 'Content-Type: application/json' \
  -d '{"query":"MATCH ?s ex:parentOf ?o FILTER ?s != ?o"}'
```

#### POST /dir

Directory service: resolve IRIs to structured metadata. Auth: none. Body `{iris: [string]}` → `{<iri>: {label|null, description|null, ...}}`.

```bash
curl -X POST http://localhost:7879/dir -H 'Content-Type: application/json' \
  -d '{"iris":["ex:parentOf","ex:person1"]}'
```

### Schema discovery (`/discovery/*`)

All `GET`, auth: none. These describe the substrate's schema, vocabulary, policies, and contract — ideal for SDK generators and curation tooling.

#### GET /discovery/contract-version

Substrate API contract version + supported clauses + action list. Returns `{contract_version, dontosrv_version, dir_version, supported_dontoql_clauses: [string], supported_sparql_subset: [string], actions: [string]}`. Supported actions: `read_metadata, read_content, quote, derive_claims, derive_embeddings, translate, summarize, export_*, train_model, publish_release, share_with_third_party, federated_query, request_deletion`.

```bash
curl 'http://localhost:7879/discovery/contract-version'
```

#### GET /discovery/contexts

List contexts (cap 5000). Returns `{contexts: [{iri, kind, parent|null, label|null, mode, closed_at|null}], limit}`.

```bash
curl 'http://localhost:7879/discovery/contexts'
```

#### GET /discovery/contexts/{iri}

Context detail with assigned policies and effective actions. Returns `{iri, kind, parent|null, label|null, mode, closed_at|null, policies: [string], effective_actions: {}}`.

```bash
curl 'http://localhost:7879/discovery/contexts/donto:evidence'
```

#### GET /discovery/predicates

List predicates by usage descending (cap 5000). Returns `{predicates: [{iri, label|null, status, minting_status, statement_count}], limit}`.

```bash
curl 'http://localhost:7879/discovery/predicates'
```

#### GET /discovery/predicates/{iri}

Predicate detail with descriptor + alignments (top 50). Returns `{iri, label|null, description|null, status, minting_status, canonical_of|null, inverse_of|null, characteristics: {symmetric, transitive, functional, inverse_functional}, descriptor: {gloss|null, subject_type|null, object_type|null, domain|null, example_subject|null, example_object|null}, statement_count, alignments: [{source_iri, target_iri, relation, confidence}]}`.

```bash
curl 'http://localhost:7879/discovery/predicates/ex:parentOf'
```

#### GET /discovery/predicate-inventory

Ranked predicate listing under an optional `?prefix=<curie>`, capped by `?limit` (default 100, clamped 1–5000). Returns `{predicates: [{predicate, namespace, n}], prefix|null, limit}`.

```bash
curl 'http://localhost:7879/discovery/predicate-inventory?prefix=ex&limit=50'
```

#### GET /discovery/predicate-namespaces

Namespace-level rollup: distinct predicates + total statements per CURIE prefix. Returns `{namespaces: [{namespace, distinct_predicates, total_statements}]}`.

```bash
curl 'http://localhost:7879/discovery/predicate-namespaces'
```

#### GET /discovery/modalities

Allowed modality values. Returns `{modalities: [{name, description}]}`.

```bash
curl 'http://localhost:7879/discovery/modalities'
```

#### GET /discovery/extraction-levels

Allowed extraction-level values (claim provenance classification). Returns `{extraction_levels: [string], observed_in_corpus: [string]}`. Standard list: `quoted, table_read, example_observed, source_generalization, cross_source_inference, model_hypothesis, human_hypothesis, manual_entry, registry_import, adapter_import`.

```bash
curl 'http://localhost:7879/discovery/extraction-levels'
```

#### GET /discovery/policies

Policy capsules (cap 1000). Returns `{policies: [{policy_iri, policy_kind, revocation_status, expiry|null, summary|null, allowed_actions: {}, inheritance_rule}]}`.

```bash
curl 'http://localhost:7879/discovery/policies'
```

#### GET /discovery/policies/{iri}

Policy capsule detail + assignment count. Returns `{policy_iri, policy_kind, authority_refs: [string], allowed_actions: {}, inheritance_rule, revocation_status, expiry|null, summary|null, labels: {}, created_at, created_by, assignments_count}`.

```bash
curl 'http://localhost:7879/discovery/policies/donto:policy-public-read'
```

#### GET /discovery/frame-types

Registered frame types (structural schemas). Returns `{frame_types: [{kind, label|null, description|null}]}`.

```bash
curl 'http://localhost:7879/discovery/frame-types'
```

#### GET /discovery/alignment-relations

The alignment-edge relation types. Returns `{alignment_relations: [{name, description}]}`. Relations: `exact_equivalent, inverse_equivalent, sub_property_of, close_match, decomposition, not_equivalent`.

```bash
curl 'http://localhost:7879/discovery/alignment-relations'
```

#### GET /discovery/identity-hypotheses

Registered identity hypotheses + clustered symbol counts. Returns `{identity_hypotheses: [{hypothesis_id, name, description|null, threshold_same, threshold_distinct, method, authority|null, status, clustered_symbols}]}`.

```bash
curl 'http://localhost:7879/discovery/identity-hypotheses'
```

#### GET /discovery/overlays

Registered consumer overlays. Returns `{overlays: [{overlay_iri, consumer_iri, table_name, owns_key, policy_inherits, fixed_policy_iri|null, bitemporal, description|null, registered_by, registered_at}]}`.

```bash
curl 'http://localhost:7879/discovery/overlays'
```

#### GET /discovery/dontoql-grammar

DontoQL grammar BNF (`text/plain`). Covers `SCOPE, PRESET, MATCH, FILTER, POLARITY, MATURITY, IDENTITY, IDENTITY_LENS, PREDICATES, MODALITY, EXTRACTION_LEVEL, TRANSACTION_TIME, POLICY ALLOWS, SCHEMA_LENS, EXPANDS_FROM, ORDER BY, WITH evidence, PROJECT, LIMIT, OFFSET`.

```bash
curl 'http://localhost:7879/discovery/dontoql-grammar' | head -30
```

#### GET /discovery/substrate-health

Substrate-level health dashboard (`donto_v_substrate_health` view; same numeric fields as `GET /metrics`). Returns `{currently_believed_statements, retracted_statements, distinct_subjects, distinct_predicates, distinct_contexts, evidence_links, contested_subject_predicate_pairs, total_contradiction_pressure, consumer_namespaces, registered_overlays, identity_hypotheses, argument_edges, anchor_coverage_pct, statements_estimated}`.

```bash
curl 'http://localhost:7879/discovery/substrate-health'
```

#### GET /discovery/openapi

The hand-written OpenAPI 3.1 specification (contract for SDK generators). Returns the JSON spec.

```bash
curl 'http://localhost:7879/discovery/openapi' | jq '.info'
```

---

## donto-admin

`https://admin.donto.org` (internal `localhost:3103`, Next.js 16) — a read-only observability console over the substrate. Every route returns the uniform envelope `{ok: boolean, data: T, at: ISO8601}` (errors: `{ok: false, error, at}`, HTTP 500), never cached. Access is via the `donto_ro` Postgres role (`SELECT`-only, `default_transaction_read_only=on`); there are no per-request credentials. `limit` params are clamped to `[1, max]`; `offset` defaults to 0 (≥0). Cheap endpoints use `reltuples` estimates (no statement-table scans). No OpenAPI spec.

### GET /api/health

Liveness probe for all observability surfaces (DB + HTTP services).

- **Response:** `{ok, data: {db: {ok, error?, target (redacted DSN), serverVersion?, currentUser?, readOnly?}, services: [{name, url, ok, detail|null}], temporalUiUrl}, at}`. Probes donto-api (:8000), dontosrv (:7879), donto-memory (:7900), and the read-only Postgres connection; degrades gracefully if any service is down.

```bash
curl -s https://admin.donto.org/api/health | jq
```

### GET /api/overview

At-a-glance health dashboard: substrate metrics + statement counts + daemon + extraction jobs.

- **Response:** `{ok, data: {health: SubstrateHealth|null, daemon: DaemonStatus, statementEstimate: number|null, jobsRunning, jobsCompleted, jobsFailed, jobsTotal}, at}`. `SubstrateHealth` carries the same fields as `donto_v_substrate_health`. Each section degrades to null/0 on a fresh donto.

```bash
curl -s https://admin.donto.org/api/overview | jq .data.health
```

### GET /api/contexts

Top contexts by statement count (from the `donto_mv_context_stats` matview) + totals.

- **Query:** `limit?` (1–500, default 50).
- **Response:** `{ok, data: {rows: [{iri, kind|null, mode|null, label|null, statementCount, openCount|null, refreshedAt|null}], totalContexts|null, totalStatements|null}, at}`.

```bash
curl -s 'https://admin.donto.org/api/contexts?limit=10' | jq .data.rows
```

### GET /api/fabric

Substrate fabric metrics: embedding coverage, closure breakdown, identity machinery, evidence, top predicates, GLM cap state.

- **Response:** `{ok, data: {coverage: {predicateEmbeddings, predicateTotal, entityEmbeddings, evidenceLinks, believedStatements, anchorPct}, closure: {total|null, byRelation: [{relation, n}]}, identity: {edges, clusters, proposals, hypotheses, memberships}, glmCap: {capUntil|null, capReason|null, glmCallsThisWindow, glmWindowStarted|null}|null, topPredicates: [{predicate, namespace, n}]}, at}`.

```bash
curl -s https://admin.donto.org/api/fabric | jq .data.topPredicates
```

### GET /api/processes

Live instance activity: extraction jobs, memory jobs, alignment/extraction runs, daemon heartbeat, service health, Temporal UI link.

- **Response:** `{ok, data: {jobs: {reachable, jobs: [ExtractionJob], running, completed, failed}, memoryJobs: {reachable, jobs: [MemoryJob], ok, failed, memorize, recall}, extractionRuns: [...], alignmentRuns: [...], daemon: DaemonStatus, daemonTicks: [DaemonTick], services: [ServiceHealth], temporalUiUrl}, at}`. Fetches extraction jobs from donto-api `/jobs`, memory jobs from donto-memory `/jobs/list.json`, daemon status from DB introspection; each source degrades gracefully.

```bash
curl -s https://admin.donto.org/api/processes | jq '.data | {jobs: .jobs, services: .services}'
```

### GET /api/jobs

Paginated list of ingested documents (generic ingestion-job explorer).

- **Query:** `q?` (search iri/label), `adapter?` (parser_version filter), `limit?` (1–200, default 50), `offset?` (default 0).
- **Response:** `{ok, data: {jobs: [{id (document_id), iri, label|null, mediaType|null, sourceKind|null, adapter|null, registeredBy|null, parserVersion|null, status|null, byteSize|null, createdAt|null, nStatements}], total, limit, offset, adapters: [{adapter, n}]}, at}`.

```bash
curl -s 'https://admin.donto.org/api/jobs?limit=10&offset=0' | jq .data.jobs
```

### GET /api/jobs/[id]

One ingested document: source + extracted statements + LLM transcript.

- **Path:** `id` (document_id UUID).
- **Response:** `{ok, data: {id, iri, label, mediaType, sourceKind, adapter, registeredBy, parserVersion, status, byteSize, createdAt, nStatements, source: string|null, sourceChars, context, statements: [{subject, predicate, object, isIri, maturity (0-7), confidence|null}], transcript: string|null, transcriptFile: string|null}, at}`. `404` if not found.

```bash
curl -s 'https://admin.donto.org/api/jobs/550e8400-e29b-41d4-a716-446655440000' | jq .data
```

### GET /api/jobs/[id]/charts

Complete chart datasets for one job: totals, maturity/polarity/evidence distributions, predicates, reach, entity shape.

- **Path:** `id` (document_id UUID).
- **Response:** `{ok, data: {totals: {statements, anchored, hypothesis, unanchored, aligned, argued, distinctPredicates, distinctSubjects, withValidTime}, maturity: [Pair] (0-7), polarity: [Pair], datatype: [Pair], evidence: [Pair], validTime: [Pair], objectKind: [Pair], predicateFreq: [{predicate, count}], hapax, contested: {subject, predicate, objects: [{object, weight, e, n}]}|null, entityShape: [{subject, predicates}], predicateReach: [{predicate, inJob, substrate, folds}], entityReach: [{entity, inJob, everywhere}]}, at}` where `Pair = {label, count}`. Bounded to 5000 claims.

```bash
curl -s 'https://admin.donto.org/api/jobs/550e8400-e29b-41d4-a716-446655440000/charts' | jq '.data.totals'
```

### GET /api/jobs/[id]/nebula

Belief Nebula data + layout for one job: claims with embeddings, contention systems, argument filaments, gas density.

- **Path:** `id` (document_id UUID).
- **Response:** `{ok, data: {claims: [{id, subject, predicate, object, isIri, maturityE (0-7), hasEvidence, evidenceCount, confidence, polarity (flags & 3), contextDomain, validFrom, validTo, validUnbounded, txFrom, txRetracted, predFamily, hasEntityEmbedding, hasPredicateEmbedding, placedByEmbedding, x (PCA), y}], systems: [{subject, predicate, objects: [{object, weight, evidenceLevel, polarity}], barycenter: [x, y]}], filaments: [{source, target, relation, strength}], gas: [{domain, density}]}, at}`. Bounded ≤5000 claims; server-side PCA of entity⊕predicate embeddings (deterministic layout).

```bash
curl -s 'https://admin.donto.org/api/jobs/550e8400-e29b-41d4-a716-446655440000/nebula' | jq '.data.claims | length'
```

### GET /api/jobs/[id]/panel/[key]

One lazy-loaded donto-capability lens for a job.

- **Path:** `id` (document_id UUID), `key` — one of `evidence-anchors`, `contradictions`, `predicate-alignment`, `embedding-neighbors`, `bitemporal`, `identity`, `cross-source-lens`, `provenance-maturity`, `entity-reach`, `outgoing-graph`.
- **Response:** `{ok, data: PanelData[], at}` (shape varies by key; e.g. `evidence-anchors`: `[{subject, predicate, object, anchor (surface_text), start_offset, end_offset, confidence, e_level}]`; `entity-reach`: `[{subject, in_job, believed_everywhere, beyond_this_job, pct_from_this_job}]`). `404` if `key` unknown; each panel bounded to 50–200 rows.

```bash
curl -s 'https://admin.donto.org/api/jobs/550e8400-e29b-41d4-a716-446655440000/panel/evidence-anchors' | jq '.data | length'
```

### GET /api/relations

Introspected list of every public relation (tables, matviews, views, partitioned parents) with sizes and statistics.

- **Response:** `{ok, data: {count, relations: [{name, kind (r/m/v/p), kindLabel, estRows, totalBytes, tableBytes, indexBytes, totalSize, tableSize, indexSize, lastAnalyze|null, lastVacuum|null, comment|null}]}, at}`. Pure `pg_catalog` query (no hardcoded table list), sorted by total relation size DESC.

```bash
curl -s https://admin.donto.org/api/relations | jq '.data.relations | length'
```

### GET /api/table/[rel]

Introspected table/matview/view details: columns, indexes, recent rows, count estimate, value distributions.

- **Path:** `rel` (relation name, validated against the live introspected allow-set). **Query:** `view?` (`all|columns|rows|count|distributions`, default `all`), `limit?` (1–200, default 50).
- **Response:** `{ok, data: {relation: Relation, columns?: [{name, type, notNull, hasDefault, isPk, position, comment|null}], indexes?: [{name, def, isPrimary, isUnique}], count?, recent?: {rows: [...], totalOffset?}, distributions?: [{column, value, count}]}, at}`. Injection-guarded (rel validated, `%I` identifier quoting). Big relations (>50k rows) return a `reltuples` estimate instead of an exact count.

```bash
curl -s 'https://admin.donto.org/api/table/donto_statement?view=columns' | jq '.data.columns'
```

### GET /api/beam

BEAM-10M extraction-firehose progress.

- **Response:** `{ok, data: {beam: {available: boolean, total (chunks enqueued), pending, leased (in-flight), done, failed, claims (total claims emitted)}}, at}`. Reads entirely from `donto_extract_queue`; degrades gracefully (`available: false`) on a donto without an extraction run.

```bash
curl -s https://admin.donto.org/api/beam | jq .data.beam
```

---

## MCP access

The **donto-memory MCP server** exposes the three core memory operations as Model Context Protocol tools, so MCP-aware agents (Claude Code, Codex, etc.) can read and write the substrate without hand-rolling HTTP calls. It is a thin front-end over donto-memory's `/recall`, `/search`, and `/memorize` endpoints; it talks to `https://memories.apexpots.com` by default (override with `DONTO_MEMORY_URL`). No auth is required (the substrate is public by design). There are two interchangeable implementations exposing the identical 3 tools: a Node ESM npm package (primary) and a stdlib-only Python single-file server.

### Tools

1. **`donto_recall(holder, query, limit=10)`** — recall a holder's own memories. Hybrid lexical + semantic, holder-scoped, bitemporal (prefers the latest `valid_from` for multi-valued attributes). Wraps `POST /recall`.
2. **`donto_search(query, context_prefix=null, limit=10)`** — substrate-wide full-text search across all contexts (genealogy, memory, research-db, …). Returns ranked results. Wraps `POST /search`.
3. **`donto_memorize(holder, text, valid_from=null, extract=true)`** — store a memory as an episodic chunk, optionally extracting semantic claims via LLM. Fast modes return immediately; slow modes are queued. Wraps `POST /memorize`.

### Install

NPM (hosted), runs the package directly:

```bash
npx -y donto-memory-mcp
```

Register with an MCP client:

```bash
codex mcp add donto-memory -- npx -y donto-memory-mcp
claude mcp add donto-memory -- npx -y donto-memory-mcp
```

Config block with a custom endpoint:

```json
{
  "mcpServers": {
    "donto-memory": {
      "command": "npx",
      "args": ["-y", "donto-memory-mcp"],
      "env": { "DONTO_MEMORY_URL": "http://127.0.0.1:7900" }
    }
  }
}
```

Python (local, stdlib-only, no dependencies):

```bash
python3 /mnt/donto-data/workspace/donto-memory/mcp/donto_mcp.py
```

MCP docs are published at `https://mcp.donto.org` (index + `/agents.md` + `/llms.txt` + `/mcp.json`).

---

## Quickstart recipes

### 1. Search the substrate, then read a claim

Find entities/claims by name, then pull the full evidence-backed claim card for one of them.

```bash
# Substrate-wide name search (via donto-memory)
curl -X POST https://memories.apexpots.com/search \
  -H 'Content-Type: application/json' \
  -d '{"query":"caroline brown","context_prefix":"ctx:genes","limit":10}'

# Take a statement_id from the rows, then fetch its claim card (via donto-api / dontosrv)
curl 'https://genes.apexpots.com/claim/<statement_uuid>'
```

### 2. Memorize, then recall (agentic memory)

Store an episodic memory with a real-world valid date, then recall it holder-scoped.

```bash
# Memorize (sync for fast modes; episodic always stored, semantic claims extracted if configured)
curl -X POST https://memories.apexpots.com/memorize \
  -H 'Content-Type: application/json' \
  -d '{"holder":"agent:my-bot","session_id":"discord:user:12345","text":"I met Annie Davis at Cooktown Festival in 1979.","valid_from":"1979-01-01"}'

# Recall what this holder knows (hybrid lexical + semantic)
curl -X POST https://memories.apexpots.com/recall \
  -H 'Content-Type: application/json' \
  -d '{"holder":"agent:my-bot","query":"who did I meet","limit":10}'
```

### 3. Run an embedding worker (lease → embed → submit)

A spare machine pulls work, computes `bge-small` (384-dim) vectors, and submits them. Remember the bearer token and the `curl/8` UA.

```bash
# 1. (Operator) top up the queue from the source table
curl -A 'curl/8' -X POST -H 'Authorization: Bearer <TOKEN>' -H 'Content-Type: application/json' \
  -d '{"target":"memory_chunk","limit":10000}' \
  https://donto.org/embed/enqueue

# 2. Lease a batch to embed
curl -A 'curl/8' -X POST -H 'Authorization: Bearer <TOKEN>' -H 'Content-Type: application/json' \
  -d '{"worker_id":"worker-1","n":100,"target":"memory_chunk"}' \
  https://donto.org/embed/lease
# -> {"batch":[{"target":"memory_chunk","item_id":"abc-123","text":"...","model":"BAAI/bge-small-en-v1.5","dim":384}, ...]}

# 3. Compute vectors locally (bge-small, 384 dims), then submit
curl -A 'curl/8' -X POST -H 'Authorization: Bearer <TOKEN>' -H 'Content-Type: application/json' \
  -d '{"items":[{"target":"memory_chunk","item_id":"abc-123","vector":[0.1,0.2]}]}' \
  https://donto.org/embed/submit
# -> {"upserted":1}

# Monitor progress
curl -A 'curl/8' -H 'Authorization: Bearer <TOKEN>' https://donto.org/embed/stats
```

### 4. Extract and ingest a document (then verify in the graph)

Preview facts with a dry run, commit them, and confirm they landed in the substrate.

```bash
# Preview (no writes)
curl -X POST https://genes.apexpots.com/extract \
  -H 'Content-Type: application/json' \
  -d '{"text":"Mary Watson was born in Cornwall in 1860. She married Robert Watson in Cooktown, Queensland in 1879.","context":"ctx:genes/mary-watson/obituary","dry_run":true}'

# Commit (extract + ingest, anchors attached to the source)
curl -X POST https://genes.apexpots.com/extract-and-ingest \
  -H 'Content-Type: application/json' \
  -d '{"text":"Mary Watson was born in Cornwall in 1860. She married Robert Watson in Cooktown, Queensland in 1879.","context":"ctx:genes/mary-watson/obituary"}'

# Verify: find the entity, then read its full history
curl 'https://genes.apexpots.com/search?q=mary+watson&limit=5'
curl 'https://genes.apexpots.com/history/ex:mary-watson?limit=50'
```

For batch/durable extraction, submit a job instead and poll:

```bash
JOB=$(curl -sX POST https://genes.apexpots.com/jobs/extract \
  -H 'Content-Type: application/json' \
  -d '{"text":"...","context":"ctx:genes/mary-watson/async"}' | jq -r '.job_id')
curl "https://genes.apexpots.com/jobs/$JOB" | jq '.status'
curl "https://genes.apexpots.com/jobs/$JOB/facts?limit=50"
```

---

*Verified 2026-06-07. Live-probed endpoints: donto-memory — `GET /health`, `GET /version`, `GET /substrate`, `GET /api`, `GET /modules`, `POST /recall`, `POST /search`, `POST /search/resources`, `GET /reconsolidate/queue`, `GET /explore/stats.json`, `GET /openapi.json`. donto-memory-mcp — `POST /recall`, `POST /search`, `POST /memorize`. donto-embed-coordinator — `GET /embed/health` (shape + Caddy routing). donto-api — `GET /search`, `GET /subjects`, `GET /contexts`, `GET /health`, `GET /version` (OpenAPI fetched live; note `GET /predicates` is shadowed by the web app on the public host and returns HTML). dontosrv — `GET /health`, `GET /version`, `GET /metrics`, `POST /assert`, `GET /discovery/contract-version`. donto-admin — `GET /api/health`, `GET /api/overview`. All other endpoints documented from source code.*
