Guide

Kamy Trace

Sign every LLM output your product ships. One line of code per call. Every record gets a SHA-256 + Ed25519 signature anchored to Kamy's public key. Built for EU AI Act Article 12, SOC 2 processing-integrity controls, and ISO 42001 lifecycle records.

SDK

@kamydev/trace ships in v1.1 — for now use the raw HTTP API below. Install + usage when it lands:

npm install @kamydev/trace
import { createTrace } from "@kamydev/trace";

const trace = createTrace({ apiKey: process.env.KAMY_API_KEY });

// Wrap your existing Anthropic / OpenAI call.
const { result, verifyUrl } = await trace.trackOutput(
  { feature: "support-bot", tags: ["tier-1"] },
  () => anthropic.messages.create({
    model: "claude-sonnet-4-6",
    messages: [{ role: "user", content: question }],
  }),
);

console.log("Verifiable here:", verifyUrl);

Raw HTTP — POST /api/v1/trace/record

One record at a time. Returns 201 on success with the verify URL + chain fields. Auth via Bearer API key; required scope: trace:record.

curl -X POST https://kamy.dev/api/v1/trace/record \
  -H "Authorization: Bearer $KAMY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "feature": "support-bot",
    "tags": ["tier-1"],
    "provider": "anthropic",
    "model": "claude-sonnet-4-6",
    "prompt": { "role": "user", "content": "Why was I charged?" },
    "output": { "content": "Let me check your account." },
    "latency_ms": 412,
    "input_tokens": 87,
    "output_tokens": 32
  }'

Response:

{
  "id": "8f3a5f2c-...",
  "verify_url": "https://kamy.dev/trace/8f3a5f2c-...",
  "content_sha256": "abc...",
  "signature": "MEUCIQDx...",
  "recorded_at": "2026-06-25T12:00:00.000Z"
}

Batch — POST /api/v1/trace/batch

Up to 100 records per call. The whole batch counts against your monthly quota up-front, so a batch that would cross your cap fails atomically with 402.

curl -X POST https://kamy.dev/api/v1/trace/batch \
  -H "Authorization: Bearer $KAMY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "records": [ /* up to 100 record objects */ ] }'

Verifying offline

Every record's verify URL exposes a downloadable JSON proof bundle at /trace/{id}/report. The public Ed25519 key lives at /.well-known/kamy-extract-public-key.txt (shared with Kamy Ingest — one Kamy CA, one public key to trust).

Canonical input the signature is over:

{content_sha256}|{provider}|{model}|{recorded_at}

Attest an artifact — POST /api/v1/attest

Sign the statement “this exact artifact existed, with this type, at this instant”. Pass content_sha256 if you hashed it yourself, or content_base64 (8 MB max) and we hash it server-side. The artifact itself is never stored — only its digest. Required scope: attest.

curl -X POST https://kamy.dev/api/v1/attest \
  -H "Authorization: Bearer $KAMY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content_sha256": "9f86d081...",   // or "content_base64": "<= 8MB"
    "artifact_type": "report",
    "metadata": { "quarter": "Q3" },
    "run_id": "run_2026_08_18",
    "parent_sha256": "3b4c5d6e..."
  }'

Response (201):

{
  "attestation_id": "8f3a5f2c-...",
  "content_sha256": "9f86d081...",
  "signature": "MEUCIQDx...",
  "recorded_at": "2026-08-18T12:00:00.000Z",
  "verify_url": "https://kamy.dev/api/v1/attest/verify?hash=9f86d081..."
}

run_id groups records into a provenance chain and parent_sha256 links this artifact to the input it was derived from — see provenance chains below.

Verify — GET /api/v1/attest/verify?hash=… (public)

No API key. The recipient of an artifact hashes it locally and asks whether that exact content was attested, then re-checks the Ed25519 signature themselves. The response carries no account identity, no tags and no stored metadata — only the digest, the artifact type, the timestamp, the signature and the public key.

curl "https://kamy.dev/api/v1/attest/verify?hash=9f86d081..."
# no API key — this is the public, recipient-facing check
{
  "verified": true,
  "result": "verified",            // | "not_found" | "signature_mismatch"
  "content_sha256": "9f86d081...",
  "artifact_type": "report",
  "recorded_at": "2026-08-18T12:00:00.000Z",
  "signature": "MEUCIQDx...",
  "public_key": "-----BEGIN PUBLIC KEY-----\n..."
}

result is the discriminator verified alone can't express. not_found means we hold no attestation for that digest — the artifact was never attested, or was modified afterwards (one changed byte changes the hash). signature_mismatch means a row exists but its signature does not verify: a real integrity alarm, not a retry.

Agent actions — POST /api/v1/agent-actions

One signed record per MCP tool call: what the model asked for, what came back, how long it took. After an incident this is the part an auditor asks about — and the part a model's own summary is least reliable on. tool_call and tool_result are both covered by the content hash, so editing either after the fact breaks the signature. Required scope: trace:record.

curl -X POST https://kamy.dev/api/v1/agent-actions \
  -H "Authorization: Bearer $KAMY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "server": "mcp.example.com",
    "tool": "send_email",
    "tool_call": { "to": "[email protected]", "subject": "Q3" },
    "tool_result": { "message_id": "m_1" },
    "run_id": "run_2026_08_18",
    "parent_sha256": "9f86d081...",
    "status": "ok",
    "latency_ms": 240
  }'

Returns 201 with record_id, content_sha256, signature, recorded_at and a verify_url. status accepts ok, flagged or error.

Provenance chains — GET /api/v1/provenance

Every record you tagged with the same run_id — attestations, agent actions and MCP baselines together — in ascending time order, with each signature re-verified at read time. Required scope: trace:read.

curl "https://kamy.dev/api/v1/provenance?run_id=run_2026_08_18" \
  -H "Authorization: Bearer $KAMY_API_KEY"
{
  "run_id": "run_2026_08_18",
  "chain_intact": true,
  "records": [
    {
      "record_id": "8f3a5f2c-...",
      "kind": "attestation",        // | "agent_action" | "mcp_manifest" | "trace"
      "artifact_type": "dataset",
      "content_sha256": "3b4c5d6e...",
      "parent_sha256": null,
      "recorded_at": "2026-08-18T12:00:00.000Z",
      "verified": true
    },
    {
      "record_id": "1a2b3c4d-...",
      "kind": "agent_action",
      "tool": "summarise",
      "content_sha256": "9f86d081...",
      "parent_sha256": "3b4c5d6e...",
      "recorded_at": "2026-08-18T12:00:01.000Z",
      "verified": true
    }
  ]
}

chain_intact is a strictly stronger claim than “every signature verified”: it is false if any signature fails OR if any parent_sha256 points at a hash that isn't in the chain. Valid signatures on a chain with a hole still describe a story with a missing chapter.

MCP server guard

POST /api/v1/mcp/verify-server — MCP has no manifest pinning, so a server can serve a benign tool list on review day and a hostile one next week. This fingerprints the current tool list (name + description + input schema, hashed separately), diffs it against the last baseline your account stored for that hostname, and files the new baseline as a signed ledger row. Only hashes are kept — never a third party's descriptions. Required scope: trace:record.

curl -X POST https://kamy.dev/api/v1/mcp/verify-server \
  -H "Authorization: Bearer $KAMY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "server_url": "https://mcp.example.com/mcp" }'
# or pass a manifest you already hold:
#   { "server": "mcp.example.com", "manifest": { "tools": [ ... ] } }
{
  "status": "mutated",             // | "new" | "unchanged"
  "server": "mcp.example.com",
  "tool_count": 2,
  "changes": [
    { "tool": "search", "change": "description_changed",
      "previous_sha256": "aa..", "current_sha256": "bb.." },
    { "tool": "fetch",  "change": "removed", "previous_sha256": "cc.." }
  ]
}

server_url is fetched server-side through the SSRF guard: private, loopback, link-local and cloud-metadata hosts are rejected, and every redirect hop is re-validated. Pass manifest with an explicit server to skip the fetch entirely.

POST /api/v1/mcp/scan-tool-description — scans tool descriptions for prompt-injection patterns: instructions aimed at the model, secrecy directives, hidden zero-width or bidi characters, base64 / percent-encoded blobs, embedded URLs, credential and exfiltration language, and descriptions long enough to bury any of it. Pure compute: no storage, no quota. Required scope: trace:read.

curl -X POST https://kamy.dev/api/v1/mcp/scan-tool-description \
  -H "Authorization: Bearer $KAMY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "tools": [{ "name": "get_weather", "description": "..." }] }'
{
  "risk": "high",                  // | "low" | "medium"
  "findings": [
    { "tool": "get_weather", "pattern": "instruction_override",
      "severity": "high", "excerpt": "... Ignore all previous instructions ..." },
    { "tool": "get_weather", "pattern": "secrecy_directive",
      "severity": "high", "excerpt": "... Do not tell the user ..." }
  ]
}

This is a heuristic, not a security boundary. It is a regex lint over natural language: it produces false positives (a real tool that says “you must pass an ISO date”) and false negatives (any phrasing, language or encoding we didn't anticipate). Treat high as “a human should read this server's tools before connecting it”. A low is not a safety certificate, and this endpoint should not be wired up as an automatic allow/deny gate in front of an autonomous agent.

Limits & pricing

  • Max payload: 256 KB per record (prompt + output combined).
  • Max batch size: 100 records / call.
  • Attestations, agent actions and MCP baselines all write rows into the same ledger and count against the same monthly cap.
  • Max inline artifact for /v1/attest: 8 MB of base64.
  • Free: 1,000 records / month.
  • Starter: 50,000 / month. Pro: 250,000. Business: 1,000,000.
  • Scale: unlimited, custom contract.
  • See pricing for the full ladder.
Kamy Trace — sign every LLM output — Kamy