# 3pm-backend route `routes`

Paths are relative to the backend's origin (https://fleet.edgecr4ft.net, or your own 3pm-backend), not to this site. Index: https://3pm.edgecr4ft.net/docs/backend/api.md

---

tokens:
  claude: 3545
  o200k: 2265
# GET /api/v1/routes (routes)

Every route this backend serves: an index for an LLM by default, with `metricsInventory` and `queryMetrics` given whole; one route in full with `?route=<name>` (details, bounds, body and answer types), the JSON table with `?format=json` or `Accept: application/json` (what this client sends); `?version=` defaults to `latest`.

- auth: none
- answer: ApiDescriptor (JSON)

## Types

```ts
/**
 * What `GET /api/v1/routes` answers this client, which asks for JSON
 * (without that, the route answers text for an LLM): the table of the
 * backend that answered. Compare `version` with the build this client was generated
 * from, or read `routes` to drive a client with no generated methods
 * at all (`Client.call` / `Client.url` take a name and arguments).
 */
interface ApiDescriptor {
  /**
   * What this document costs to put in a prompt, before what it says —
   * over the compact JSON the route sends, this field included (not the
   * pretty form `assets/api.json` is written in).
   */
  tokens: TokenCount;
  /** The backend's own version — never `"latest"`. */
  version: string;
  /** How live data is had: the SSE feed and the agent's WebSockets, by route name. */
  realtime: Realtime;
  /** What the query routes refuse past, by route name. */
  limits: ApiLimits;
  /**
   * Where this client lives and how to use it. The URLs are relative:
   * they resolve against whatever origin served the descriptor.
   */
  client: {
    /** The ES module — `/api/v1/client.js`. */
    url: string;
    /** These types — `/assets/api.d.ts`. */
    types: string;
    /** This document — `/api/v1/routes`. */
    descriptor: string;
    /** A runnable snippet: construct, call, subscribe. */
    example: string;
  };
  routes: RouteDescriptor[];
}

/** What an answer costs to put in a prompt: the first field of every answer that carries it (`metricsInventory`, `GET /api/v1/routes`). */
interface TokenCount {
  /** Claude's tokenizer, estimated from the bytes with a ratio measured per shape through the `count_tokens` API — within a few percent. */
  claude: number;
  /** OpenAI's `o200k_base`, exact. Claude counts 1.5–1.7× more on this backend's answers. */
  o200k: number;
}

/**
 * The real-time surface by role, pointing into `ENDPOINTS` / `routes`
 * by name: how live data is had, without filtering the table on `kind`.
 */
interface Realtime {
  feed: RealtimeFeed;
  agent: RealtimeAgent;
}

/**
 * What a query body may ask for, by route name: the very constants the
 * backend validates against, so a caller reads its bounds instead of
 * discovering them one 400 at a time.
 */
interface ApiLimits {
  queryMetrics: MetricsQueryLimits;
  eventsQuery: EventsQueryLimits;
  logsQuery: LogsQueryLimits;
}

/** One route of `GET /api/v1/routes`: an `EndpointMeta` that names itself. */
interface RouteDescriptor extends EndpointMeta {
  name: string;
  /**
   * The long form, where the one-line `doc` is not enough: a route
   * whose body is a small language of its own carries what each field
   * means, what comes back, and an example — everything needed to build
   * a request from this document alone. `null` on the plain routes.
   */
  details: string | null;
}

/** The one feed a page reads: `Client.events()` / the `iter` method. */
interface RealtimeFeed {
  /** The `sse` route, by name — `events`. */
  route: EndpointName;
  transport: "sse";
  eventKinds: EventKind[];
  /** The client's async-iterator method for it — `follow`. */
  iter: string;
}

/**
 * What `enroll` and `link` speak: the agent's wire protocol (`3pm
 * link`), not a feed a page can read. `enroll()` and `link()` throw an
 * `AgentOnlyError` pointing at `events()`; `wsUrl()` is for an agent.
 */
interface RealtimeAgent {
  /** The `agent-ws` routes, by name — `enroll` and `link`. */
  routes: EndpointName[];
  transport: "websocket";
  /** The WebSocket sub-protocol both sides negotiate — `3pm.link`. */
  subprotocol: string;
  /** The link versions this backend accepts. */
  min: number;
  max: number;
  /** Where the protocol is written down: a path from the root of the `3pm-rd` workspace, not a URL. */
  specPath: string;
}

/** What `queryMetrics` refuses past — `options`, the range and the shape of a spec. */
interface MetricsQueryLimits {
  /** 31 days. */
  max_range_ms: number;
  max_points: number;
  /** Applied when `options.max_points` is absent. */
  default_points: number;
  max_series: number;
  /** Applied when `options.max_series` is absent. */
  default_series: number;
  max_queries: number;
  max_formulas: number;
  /** Per query spec. */
  max_filters: number;
  /** Per `in` / `nin` filter. */
  max_values: number;
  /** Characters, per filter value. */
  max_value_len: number;
  /** The body is refused before it is parsed past this. */
  max_body_bytes: number;
  /**
   * The one bound not carried here: `range.step` may not go below the
   * store's resolution, which depends on how the backend was started.
   * The value of this field is where to read it — `store.interval_ms`
   * of `GET /api/v1/info`.
   */
  step_floor_from: string;
}

/** What `eventsQuery` refuses past. */
interface EventsQueryLimits {
  /** 31 days. */
  max_range_ms: number;
  max_limit: number;
  /** Applied when `limit` is absent. */
  default_limit: number;
  /** Characters, per filter value. */
  max_value_len: number;
  max_body_bytes: number;
}

/** What `logsQuery` refuses past. */
interface LogsQueryLimits {
  /** 30 days, the engines' retention. */
  max_range_ms: number;
  max_templates: number;
  default_templates: number;
  max_lines: number;
  default_lines: number;
  /** Bytes, per filter value and `text`. */
  max_value_len: number;
  max_body_bytes: number;
}

/** One entry of the generated endpoint map. */
interface EndpointMeta {
  method: "GET" | "POST" | "DELETE";
  /** The path with its arguments named, e.g. `/api/buckets/{clusterId}/nodes`. */
  path: string;
  args: readonly string[];
  kind: "json" | "sse" | "agent-ws" | "static";
  /** Behind the cross-site guard: another site's page is refused, unless it carries a bearer token on a `user` route. */
  guarded: boolean;
  /** `user`: needs a login (and, under a bucket, a bucket the account owns); `public`: anyone. */
  auth: "public" | "user";
  /**
   * Also open to a node's own bearer (`3pma_…`), when the node's row
   * carries this scope (`setNodeAccess`); `null` for a route only an
   * account may call. A node is a second kind of credential the account
   * layer admits, not a third kind of route.
   */
  node: NodeScope | null;
  body: string | null;
  returns: string;
  /** For an `sse` route, the name of its async-iterator twin (`follow` for `events`). */
  iter: string | null;
  doc: string;
}

type EndpointName =
  | "info"
  | "routes"
  | "client"
  | "register"
  | "login"
  | "logout"
  | "me"
  | "createUserToken"
  | "revokeUserToken"
  | "listBuckets"
  | "createBucket"
  | "getBucket"
  | "setBucketSettings"
  | "bucketToken"
  | "events"
  | "listNodes"
  | "approveNode"
  | "revokeNode"
  | "deleteNode"
  | "setNodeAccess"
  | "listTokens"
  | "createToken"
  | "revokeToken"
  | "queryMetrics"
  | "metricsCatalog"
  | "metricLabelValues"
  | "metricsInventory"
  | "metricsByServer"
  | "metricsByApp"
  | "eventsQuery"
  | "logsQuery"
  | "insights"
  | "insight"
  | "nodeAppMetrics"
  | "nodeActions"
  | "nodeHost"
  | "nodeProcessTrees"
  | "nodeProcessTree"
  | "nodeAction"
  | "nodeShell"
  | "nodeForward"
  | "nodeListen"
  | "nodeCall"
  | "putCallArtifact"
  | "getCall"
  | "listArtifacts"
  | "artifactFile"
  | "putBlob"
  | "getBlob"
  | "viewBlob"
  | "enroll"
  | "link"
  | "pushLogs"
  | "indexPage"
  | "bucketPage"
  | "bucketSettingsPage"
  | "asset";

type EventKind = "init" | "node" | "telemetry" | "emit" | "call" | "artifact" | "reset";

/**
 * What a node may ask of its bucket with its own bearer, granted by the
 * owner (`setNodeAccess`): `fleet:list` the fleet as a list, `fleet:read`
 * its metrics, hosts, trees and action catalogue, `fleet:invoke` placing
 * calls. Nothing by default.
 */
type NodeScope = "fleet:list" | "fleet:read" | "fleet:invoke";
```
