10-minute quickstart

Try it now, no signup, no credit card. Create an anonymous project token, get a signed chain in 30 seconds, keep the chain when you decide to upgrade.

1. Create an anonymous project:
curl -X POST https://etch.systems/v1/your-project
2. Point any MCP client at the returned mcp_endpoint with the returned token. Claude Code / Cursor / Continue / Cline config example:
{
  "mcpServers": {
    "etch": {
      "url": "https://etch.systems/mcp",
      "headers": {"Authorization": "Bearer <YOUR_ANON_TOKEN>"}
    }
  }
}
3. Every write your agent makes lands on a real signed chain (Ed25519 + FIPS 205 SLH-DSA hybrid). 500 events, 14 days retention. Convert to a paid project at any time to keep the chain forever.

Per-client config paths for Claude Code / Cursor / Continue / Cline / Codex live at /docs/mcp-clients.
Ready for a persistent project? 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.

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, if the one-liner is blocked

If the curl one-liner is blocked (corporate firewall, etc.), email support@etch.systems and we will send the installer bundle.


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-recorder.js and import it wherever you make LLM calls.

Step 1: The reusable client

// etch-recorder.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-recorder.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.