CClawMetryDocs

Runtimes

Custom runtime ingest API#

Push events from any runtime into ClawMetry without writing to a supported runtime's filesystem layout. Designed for in-house agents, eval harnesses and web agents that already produce structured run and step records.

Tier: Pro (entitlement key custom_runtime_ingest).

Endpoints#

MethodPathDescription
GET/api/v1/runtimesList runtimes ClawMetry knows about. Free.
POST/api/v1/runsOpen a run; returns a run_id.
POST/api/v1/runs/<id>/eventsAppend one event or many.
POST/api/v1/runs/<id>/endMark the run ended. Optional.
GET/api/v1/runs/<id>Read back: was the run persisted?

Authentication#

Two modes, selected by whether a token is configured:

  1. Localhost only (default). With CLAWMETRY_INGEST_TOKEN unset, only

loopback requests are accepted. Zero configuration, local only.

  1. Token header. Set CLAWMETRY_INGEST_TOKEN=<secret> on the dashboard;

clients then send X-ClawMetry-Token: <secret>. The comparison is constant-time.

A non-loopback request without a matching token gets 401 unauthorized.

Quickstart#

bash
# 1. Open a run
curl -s http://localhost:8900/api/v1/runs \
  -H 'content-type: application/json' \
  -d '{"runtime": "my_engine", "metadata": {"build": "abc123"}}'
# -> {"ok": true, "run_id": "run_a1b2c3d4…", "runtime": "my_engine"}

# 2. Push an event
curl -s http://localhost:8900/api/v1/runs/run_a1b2c3d4/events \
  -H 'content-type: application/json' \
  -d '{"event": {
        "id":         "evt_1",
        "ts":         '"$(date +%s)"',
        "event_type": "model.completed",
        "model":      "claude-sonnet-5",
        "data":       {"input_tokens": 1240, "output_tokens": 312}
      }}'
# -> {"ok": true, "accepted": 1, "ids": ["evt_1"]}

# 3. Close it
curl -s http://localhost:8900/api/v1/runs/run_a1b2c3d4/end \
  -H 'content-type: application/json' -d '{}'

Event payload#

FieldRequiredNotes
idnoDedupe key. The server fills evt_<hex> if absent. Re-ingesting the same id is a no-op.
tsnoEpoch seconds (float). Server uses now if absent.
event_typenoFree-form. Conventional: session.started, prompt.submitted, model.completed, tool.invoked, tool.result, error.
session_idnoDefaults to the run_id.
tool_namenoWhen the event represents a tool call.
modelnoLLM model id — this is what makes cost derivable.
rolenouser, assistant, system, tool.
datanoOpaque dict. The daemon adds data.extra.runtime.

Token counts belong in data as input_tokens, output_tokens and, where you have them, cache_read_tokens / cache_write_tokens. Supplying model alongside them is what lets ClawMetry derive cost — without a model id you get tokens and an honest unknown for dollars.

Batching#

json
{"events": [
  {"id": "evt_1", "event_type": "prompt.submitted", "role": "user"},
  {"id": "evt_2", "event_type": "tool.invoked", "tool_name": "search"},
  {"id": "evt_3", "event_type": "model.completed", "model": "gpt-5.4",
   "data": {"input_tokens": 900, "output_tokens": 120}}
]}

Idempotency#

Event ids are the dedupe key, and re-posting an id already stored is a no-op rather than an error. This is deliberate: a harness that retries a failed push, or replays a run from a log, must not double-count. Generate stable ids from something in your own domain — a step index, a request id — rather than a random value per attempt.

A worked example#

python
import os, time, uuid, requests

BASE = os.environ.get("CLAWMETRY_URL", "http://localhost:8900")
HDRS = {"content-type": "application/json"}
if os.environ.get("CLAWMETRY_INGEST_TOKEN"):
    HDRS["X-ClawMetry-Token"] = os.environ["CLAWMETRY_INGEST_TOKEN"]

run = requests.post(f"{BASE}/api/v1/runs", json={
    "runtime": "research_agent",
    "metadata": {"repo": "acme/api", "trigger": "nightly"},
}, headers=HDRS).json()
rid = run["run_id"]

def emit(step, **event):
    event.setdefault("id", f"{rid}-{step}")
    event.setdefault("ts", time.time())
    requests.post(f"{BASE}/api/v1/runs/{rid}/events",
                  json={"event": event}, headers=HDRS)

emit(0, event_type="prompt.submitted", role="user",
     data={"text": "summarise last night's failures"})
emit(1, event_type="tool.invoked", tool_name="query_ci",
     data={"arguments": {"since": "24h"}})
emit(2, event_type="model.completed", model="claude-sonnet-5", role="assistant",
     data={"input_tokens": 4210, "output_tokens": 380})

requests.post(f"{BASE}/api/v1/runs/{rid}/end", json={}, headers=HDRS)

Those events land in the same store as every other runtime, appear in Conversations and Cost, and are visible to the MCP server — so your coding agent can ask about your production agent's night.

Reading it back#

bash
curl -s http://localhost:8900/api/v1/runs/run_a1b2c3d4 | jq
curl -s 'http://localhost:8900/api/local/events?session_id=run_a1b2c3d4' | jq '.rows'

If a run reads back but nothing appears in the dashboard, check that the daemon is running — the ingest endpoint accepts the write, but the rollups the UI reads are built by the daemon.

Cookie preferences