Etch

10-minute quickstart

Don't have a project yet? Sign up in 30 seconds to get your project_id, app_token, and op_token. Free tier, no credit card, one project.

Etch is language-agnostic, the API is HTTP + JSON-RPC (MCP protocol). Pick the integration path that matches what you're building. Every path lands events in the same authenticated audit chain.

Pick your path:

What you need for any path

Path A. Your team is building through AI coding agents

Every Read / Edit / Bash / Grep / Write your team runs through Claude Code (or Cursor, Cline, Codex, Continue) records to Etch's authenticated audit chain. Zero source code changes to your app. Per-developer install, five minutes each.

One-line install (recommended)

Team lead shares this with every developer. Each dev pastes it into their terminal once:

curl -sSL https://etch.systems/install/claude-code.sh | bash -s -- \
    --project YOUR_PROJECT_ID \
    --token wm_your_app_token_here

The installer:

Then each developer runs:

# Quit and reopen Claude Code so hooks take effect
Cmd-Q → reopen Claude Code

What lands in Etch after that

Every tool call the AI coding agent makes on any codebase:

The compliance answer this delivers

"Prove our design docs / DB schemas / production data never leaked to Anthropic during development." → the authenticated audit chain has every file path the agent read; if Read /path/to/production.db is absent, production data never went to the LLM.

Alternative, clone the repo

If the curl one-liner is blocked (corporate firewall, etc.):

git clone https://github.com/SaravananJaichandar/etch.git
cd etch/integrations
./etch_install.py claude-code --project YOUR_PROJECT_ID --token wm_...

Path B. Python app that makes LLM calls in production

Wrap your existing Anthropic / OpenAI SDK. One-line change; every prompt + response records automatically. Also covers OpenAI-compat providers (Groq, OpenRouter, HuggingFace router, Perplexity, Fireworks, Cerebras, together.ai, DeepSeek).

from etch.integrations.anthropic_client import EtchAnthropic

client = EtchAnthropic(
    api_key="sk-ant-...",
    etch_app_token="wm_...",
    etch_project_id="your-project",
)

response = client.messages.create(
    model="claude-opus-4-7",
    max_tokens=1024,
    messages=[{"role": "user", "content": "hi"}],
)

What records per call: model, input hash, output hash, token counts, stop_reason, tool_calls_seen, dev_id. Full prompt/response content persisted by default for reconstructability; opt out with ETCH_PERSIST_HASH_ONLY=1.

For OpenAI-compatible providers

from etch.integrations.openai_client import EtchOpenAI

client = EtchOpenAI(
    api_key="gsk_...",
    openai_kwargs={"base_url": "https://api.groq.com/openai/v1"},
    provider_label="groq",   # tags events under "groq:model" in dashboard
    etch_app_token="wm_...", etch_project_id="your-project",
)

Full integration docs at /docs/integrations.


Path C. Node.js / TypeScript app that makes LLM calls

Node 18+ has fetch built in. No dependencies needed. Save this as etch-client.js and import it wherever you make LLM calls.

Step 1: The reusable client

// etch-client.js
export class EtchClient {
  constructor({ appToken, projectId, baseUrl = "https://etch.systems" }) {
    this.appToken = appToken;
    this.projectId = projectId;
    this.baseUrl = baseUrl;
    this.mcpSessionId = null;
  }

  async _initSession() {
    const headers = {
      "Authorization": `Bearer ${this.appToken}`,
      "Content-Type": "application/json",
      "Accept": "application/json, text/event-stream",
    };
    const initRes = await fetch(`${this.baseUrl}/mcp`, {
      method: "POST", headers,
      body: JSON.stringify({
        jsonrpc: "2.0", id: 1, method: "initialize",
        params: { protocolVersion: "2025-11-05", capabilities: {},
                  clientInfo: { name: "my-app", version: "0.1" } },
      }),
    });
    if (initRes.status !== 200) throw new Error(`Etch init failed: ${initRes.status}`);
    this.mcpSessionId = initRes.headers.get("mcp-session-id");
    await fetch(`${this.baseUrl}/mcp`, {
      method: "POST",
      headers: { ...headers, "mcp-session-id": this.mcpSessionId },
      body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
    });
  }

  async recordEvent({ sessionId, description, entities = [], reasoning = "",
                     evidence = {}, success = true }) {
    if (!this.mcpSessionId) await this._initSession();
    const res = await fetch(`${this.baseUrl}/mcp`, {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${this.appToken}`,
        "Content-Type": "application/json",
        "Accept": "application/json, text/event-stream",
        "mcp-session-id": this.mcpSessionId,
      },
      body: JSON.stringify({
        jsonrpc: "2.0", id: Date.now(), method: "tools/call",
        params: { name: "record_event", arguments: {
          event_type: "tool_call",   // must be from enum: file_edit | file_create | file_delete | test_run | lint_run | user_correction | tool_call
          session_id: sessionId,
          entities,
          description,
          reasoning,
          evidence,
          success,
        } },
      }),
    });
    if (res.status === 401) {
      this.mcpSessionId = null;   // session expired, retry once
      return this.recordEvent(arguments[0]);
    }
    return res.status === 200;
  }
}

Step 2: Wrap your existing LLM call

// example.js: wrapping an OpenAI call
import { EtchClient } from "./etch-client.js";
import OpenAI from "openai";

const etch = new EtchClient({
  appToken: process.env.ETCH_APP_TOKEN,
  projectId: process.env.ETCH_PROJECT_ID,
});
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

async function myAgentCall(userId, input) {
  const messages = [{ role: "user", content: input }];
  const response = await openai.chat.completions.create({
    model: "gpt-4o", messages,
  });

  await etch.recordEvent({
    sessionId: `user-${userId}-request-${Date.now()}`,
    description: `OpenAI call for user ${userId}`,
    entities: [`user:${userId}`],
    reasoning: "gpt-4o completion",
    evidence: {
      tool_name: "openai:gpt-4o",
      tool_input: { message_count: messages.length,
                    input_tokens: response.usage.prompt_tokens },
      tool_output: { output_tokens: response.usage.completion_tokens,
                     finish_reason: response.choices[0].finish_reason },
    },
  });

  return response.choices[0].message.content;
}

Path D. Any language (raw HTTP)

Etch's API is MCP over HTTPS. Three JSON-RPC calls, any HTTP client.

1. Initialize

POST https://etch.systems/mcp
Authorization: Bearer wm_...
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
  "protocolVersion":"2025-11-05","capabilities":{},
  "clientInfo":{"name":"my-app","version":"0.1"}}}

Response includes header mcp-session-id: <uuid>. Save it for calls 2 + 3.

2. Notify initialized (no response needed)

POST https://etch.systems/mcp
Authorization: Bearer wm_...
Content-Type: application/json
Accept: application/json, text/event-stream
mcp-session-id: <from step 1>

{"jsonrpc":"2.0","method":"notifications/initialized"}

3. Record an event

POST https://etch.systems/mcp
Authorization: Bearer wm_...
Content-Type: application/json
Accept: application/json, text/event-stream
mcp-session-id: <from step 1>

{"jsonrpc":"2.0","id":42,"method":"tools/call","params":{
  "name":"record_event",
  "arguments":{
    "event_type":"tool_call",
    "session_id":"hello-world",
    "entities":["test-entity"],
    "description":"my first etch event",
    "success":true
  }}}

Common gotchas (any path)

The event_type enum

event_type must be exactly one of these, anything else is silently rejected (returns HTTP 200 but the tool errors and the event never persists):

file_edit  |  file_create  |  file_delete
test_run   |  lint_run
user_correction
tool_call

For general LLM / tool-call recording, tool_call is the right value.

Auth errors

Session id

session_id groups events together. For a customer-support agent, use one session_id per ticket. For a coding agent, one per conversation. For a matrimony match-analysis call, one per match. It's the primary axis for the Sessions dashboard.

Verify events landed

Open the dashboard, log in with your op_token, click Sessions. Your event appears within seconds, grouped by session_id.

Verify the signatures

Anyone can verify without asking you. Two ways:

From the dashboard: click Verify in the left nav. Each closed epoch has a "Copy verify command" button. Paste into your terminal.

From anywhere:

curl -s https://etch.systems/projects/YOUR_PROJECT/audit-log/dump/manifest

The response is a signed JSON manifest containing the SHA-256 digest and hybrid signatures over the entire log. Downloadable, verifiable offline with etch-verify after download.

Use-case patterns

Etch is domain-neutral. Pick whichever of these matches your product; ignore the rest.

Support

Email support@etch.systems. First-month check-ins are on the house.