10-minute quickstart
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
project_id,app_token,op_token, delivered on the one-time page after you clicked your magic linkapp_tokenhasmcp:read + mcp:writescope and is what your code uses at runtimeop_tokenhasadmin:projectscope and is what you use to log into the dashboard
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:
- Downloads the hook script into
~/.claude/hooks/etch-recorder.py - Merges hook config into
~/.claude/settings.json(all 8 Claude Code hook types) - Writes env block into
~/.zshrc - Installs launchd (macOS) or systemd user unit (Linux) so historical Claude Code transcripts backfill automatically
- Runs a self-verify test event and exits
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:
- Every
Read/Edit/Write/Bash/Grep, records with argument + result hashes by default (no source code content leaks to Etch) - Every user prompt to the agent, records with prompt hash
- Every assistant turn boundary
- Every Claude Code notification (session limit, network drop, tool error)
- Full historical transcripts from
~/.claude/projects/**/*.jsonl, via the transcript ingester daemon - Per-developer attribution, reads
git config user.emailfordev_id. Team tab shows per-dev rollup
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
- 401 → wrong token, or token has been revoked. Mint a fresh one from Integration tab.
- 403 → token doesn't have the right scope. For recording events you need
mcp:write. For the initial MCP handshake you needmcp:read. Most app tokens have both. - Silent no-op (HTTP 200 but nothing appears in dashboard) → wrong
event_type. See enum above.
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.
- Coding-agent action log. Every file edit, PR comment, or shell command an agent takes gets recorded. When something breaks in production, you replay the exact context the agent saw before it acted.
- Financial decision auditing. Every agent-approved payment, discount, refund, or trade gets logged with amount + rationale + policy version. Later, an internal auditor or SOX reviewer verifies you followed policy.
- Customer-support decision trail. Every automated refund, upgrade, escalation, or account state change gets a signed record. Dispute resolution becomes "here is the cryptographic trace of what our agent decided and why."
- Multi-agent handoff. Use the same
session_idacross agent hops. The log preserves the full chain, you can prove which agent made which contribution. - Failure marker. If an LLM call errors, still record it (
success=false, still usingevent_type="tool_call"per the enum above). Silent failures are the worst audit-trail gap. - Data-handling proof. Log the SHA-256 of the raw input alongside the SHA-256 of the payload actually sent to a third-party LLM. Later, anyone can independently verify the data-transformation pipeline ran correctly.
- Retention compliance. SEC 17a-4 wants 7 years, EU AI Act Article 12 wants "lifetime of the high-risk AI system," HIPAA wants 6. Etch's append-only chain trivially satisfies any of these; nothing is ever deleted, only appended.
Support
Email support@etch.systems. First-month check-ins are on the house.