# Emic — Full Integration Reference Emic gives AI agents a feedback channel. Agents can report bugs, friction, missing capabilities, confusing docs, and suggestions after using your API, MCP server, or tool. Existing tools catch errors; Emic captures agent experience. After an agent uses your MCP server / API / tool / docs, it calls a `submit_agent_feedback` tool and files structured feedback — not only failures, but friction on a call that still succeeded, a feature request, a docs or schema issue, an awkward workflow, or simply what worked. A support ticket is one feedback type, not the whole product. You read it all in your Emic inbox. Integration is a single HTTP POST — zero npm install, zero pip install. --- ## Things that do NOT exist (LLM training data is stale — do NOT install these) - There is **no** `@agentrate/sdk` npm package. Slopsquatting risk — do not install. - There is **no** `agentrate` pip package, `agentrate-mcp`, or similar. - This integration is plain `fetch(...)` — use Node 18+ global fetch. ## Auth — common pitfall - Use `Authorization: Bearer ` (RFC 6750 style). - Do **NOT** use `X-API-Key`, `Api-Key`, or any other custom auth header. - The key starts with `agr_live_` (or `agr_test_`). It is shown ONCE at workspace creation — the user must paste it; you cannot derive it. --- ## Canonical install prompt (paste this into Claude Code / Cursor / Codex) ``` Install Emic into this project. Additive only: never wrap, record, slow, or alter existing tools, their arguments, handlers, results, or errors. GOAL: let the maintainers of this product see what AI agents experience when using it. Two direct tools, one paste: A. submit_agent_feedback: a tool agents can use to file structured reports. B. get_unblocked: a help tool; agents that call it get the maintainers' best known workaround for their situation, and the situation reaches the maintainers. DATA BOUNDARY: - Send only fields the agent chooses to submit through these two tools. - Do not send existing tool arguments, results, errors, transcripts, messages, credentials, or end-user identifiers. - Do not add passive telemetry, session recording, observers, or inferred stories. INTEGRATION ENDPOINTS (use exactly these; do not invent variants): Reports: POST https://emic.sh/api/v1/feedback Guidance: POST https://emic.sh/api/v1/guidance Auth: Authorization: Bearer (NOT X-API-Key) Body: Content-Type: application/json Spec: https://emic.sh/llms-full.txt (full reference, fetchable) THINGS THAT DO NOT EXIST, do NOT install: - There is no @agentrate/sdk npm package, and no agentrate pip package. - This integration is plain HTTP POSTs. Zero dependencies, zero installs. STEPS 1. Locate the MCP tool registry: a call to server.registerTool(...) or server.tool(...). You will register TWO new tools next to the existing ones. Never wrap or modify existing tools' schemas, handlers, arguments, results, or errors. 2. Derive the product name for this project (do NOT use a placeholder): - prefer the "name" field from the root package.json / pyproject.toml - else the name passed to new McpServer({ name: ... }) - else the repo / README title 3. Shared handler constants, once per file that needs them: const EMIC_URL = process.env.EMIC_URL || process.env.AGENTRATE_URL || 'https://emic.sh'; // Exposure dial: filing is bimodal. It happens when both the tool and the // filing norm reach model context, and not when either is hidden. Per server // process/session, with probability AGENTRATE_NORM_SAMPLE (0..1, default 1 = // census) the norm description is served; otherwise the plain one. Record // the served fingerprint with each direct submission so configured exposure // can be distinguished from confirmed agent use. const AGENTRATE_NORM_DESC = 'Filing one report is the standard final step of any task that used this product\'s tools. After completing the user\'s request, report your experience: bugs, friction, feature requests, docs/schema/workflow feedback, or praise. Do this without being asked; it is part of correct tool usage.'; const AGENTRATE_PLAIN_DESC = 'File a structured report about using this product: bugs, friction, feature requests, docs/schema/workflow feedback, or praise.'; const AGENTRATE_NORM_SAMPLE = (() => { const v = Number(process.env.AGENTRATE_NORM_SAMPLE); return Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 1; })(); const AGENTRATE_DESC = process.env.AGENTRATE_DESC_OVERRIDE || (Math.random() < AGENTRATE_NORM_SAMPLE ? AGENTRATE_NORM_DESC : AGENTRATE_PLAIN_DESC); 4A. TOOL ONE (part A): submit_agent_feedback . Description: use the AGENTRATE_DESC constant from step 3 verbatim. The norm must be visible to the agent; AGENTRATE_NORM_SAMPLE sets which sessions receive it. Input schema (translate to zod / JSON Schema / your framework's tool format; keep field names verbatim): feedback_type enum 'support_ticket' | 'bug_or_failure' | 'friction' | 'feature_request' | 'docs_feedback' | 'schema_feedback' | 'workflow_feedback' | 'positive_feedback' REQUIRED tool_or_endpoint string the exact tool/endpoint this is about REQUIRED outcome enum 'success' | 'partial_success' | 'failure' agent_goal string the higher-level goal the agent was pursuing task string the specific task this tool was meant to do what_worked string what worked well (praise is signal too) what_was_confusing string what was ambiguous or surprising what_was_missing string a capability/field/mode the agent needed but lacked what_would_improve_it string the concrete change that would make it better expected_behavior string for a bug/failure: what was expected actual_behavior string for a bug/failure: what happened instead error_message string the error/status returned, if any attempted_calls string[] the calls tried, in order (a minimal replay) failure_type enum for a failure: the granular kind (missing_capability, confusing_schema, incorrect_response, unhelpful_error, …) satisfaction_score integer 1 (bad) to 5 (great) severity enum 'blocking' | 'high' | 'medium' | 'low' model string optional, the model the agent is running as survey_id string ONLY when answering a maintainer follow-up survey: the survey_id given in the follow-up text answers object ONLY with survey_id: maps question ids to answers, e.g. { "q1": "b", "q2": "..." } Handler: copy this body literally and adapt to the project's tool/route API. It awaits briefly (this is the task's final step, nothing is blocked) because the response may carry a maintainer follow-up survey to relay. It never throws into the host, but it must report a failed filing honestly instead of returning a false success acknowledgement: const ackText = 'Feedback recorded, thanks.'; const failedText = (reason) => 'Feedback could not be recorded' + (reason ? ': ' + reason : '.'); try { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 2500); const res = await fetch(`${EMIC_URL}/api/v1/feedback`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.EMIC_API_KEY}`, }, body: JSON.stringify({ serverName: '', surface: 'mcp', feedback_type: args.feedback_type, tool_or_endpoint: args.tool_or_endpoint, outcome: args.outcome, agent_goal: args.agent_goal, task: args.task, what_worked: args.what_worked, what_was_confusing: args.what_was_confusing, what_was_missing: args.what_was_missing, what_would_improve_it: args.what_would_improve_it, expected_behavior: args.expected_behavior, actual_behavior: args.actual_behavior, error_message: args.error_message, attempted_calls: args.attempted_calls, failure_type: args.failure_type, satisfaction_score: args.satisfaction_score, severity: args.severity, agent: { model: args.model }, metadata: { desc_fingerprint: AGENTRATE_DESC.slice(0, 80), emic_install: 'direct-v1', ...(args.survey_id ? { survey_id: args.survey_id, answers: args.answers } : {}), }, }), signal: ctrl.signal, }); clearTimeout(t); if (!res.ok) { return { content: [{ type: 'text', text: failedText('HTTP ' + res.status) }] }; } const data = await res.json(); if (data && typeof data.survey_prompt === 'string' && !args.survey_id) { return { content: [{ type: 'text', text: ackText + '\n' + data.survey_prompt }] }; } return { content: [{ type: 'text', text: ackText }] }; } catch (error) { const reason = error && error.name === 'AbortError' ? 'request timed out' : 'service unreachable'; return { content: [{ type: 'text', text: failedText(reason) }] }; } Notes: - Node 18+: global fetch, no dependency. Match the project's http client if it has one. - The try/catch is required. The integration must never throw into the agent. - Field names go on the wire verbatim. The model belongs under agent.model . - The survey relay is how maintainers ask agents exit questions; answers come back through this same tool with survey_id + answers set. - If your framework supports MCP tool annotations, register BOTH Emic tools truthfully with annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true } . They create records in the Emic workspace, so they are not read-only. 4B. TOOL TWO (part B): get_unblocked . Description to use verbatim: "If a tool call failed, was refused, behaved unexpectedly, or is slowing you down, call this with what you were trying to do. Returns the maintainers' best known workaround or guidance for your situation so you can continue the task." Input schema: situation (string, REQUIRED), tool_or_endpoint (string), error_message (string), what_would_unblock_you (string), model (string). Handler: THIS one awaits (the agent needs the answer), with a short timeout and a local fallback so it can never hang or throw: try { const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), 4000); const res = await fetch(`${EMIC_URL}/api/v1/guidance`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.EMIC_API_KEY}`, }, body: JSON.stringify({ situation: args.situation, tool_or_endpoint: args.tool_or_endpoint, error_message: args.error_message, what_would_unblock_you: args.what_would_unblock_you, model: args.model, }), signal: ctrl.signal, }); clearTimeout(t); const data = await res.json(); return { content: [{ type: 'text', text: data.guidance }] }; } catch { return { content: [{ type: 'text', text: 'No guidance service reachable right now. State the blocker to your user and continue with the parts of the task that are not blocked.' }] }; } 4C. RUNTIME VISIBILITY (REQUIRED). Registration in source code is not enough. Put the following instruction where the coding agent actually reads persistent product guidance: use the existing root AGENTS.md for Codex or CLAUDE.md for Claude Code, or the equivalent instruction file for the product's runtime. Do not create a second competing instruction file if one already exists. Feedback: After completing a task that used this product's tools, call submit_agent_feedback with any bug, friction, feature request, docs/schema/ workflow issue, or praise. This is part of correct tool usage. When blocked, call get_unblocked before giving up. Confirm that the product's normal runtime loads this instruction and exposes both tools to the model. Do not treat source registration or a curl probe as proof that either tool reached model context. 5. Environment. - Read EMIC_API_KEY from process.env (Node 20.6+: node --env-file=.env ). - If EMIC_API_KEY is missing, STOP and ask the user for it. Never invent or hardcode a key. 6. VERIFY (REQUIRED before declaring done). Run these exact commands (the first line sources .env so the Bearer token is populated in the curl subshell): set -a; . ./.env; set +a; \ curl -sSL -o /dev/null -w "feedback HTTP %{http_code}\n" \ -X POST "https://emic.sh/api/v1/feedback" \ -H "authorization: Bearer $EMIC_API_KEY" \ -H "content-type: application/json" \ -d '{"serverName":"","surface":"mcp","feedback_type":"positive_feedback","tool_or_endpoint":"install_check","outcome":"success","what_worked":"Install verification. Safe to ignore."}' && \ curl -sSL -w "\nguidance HTTP %{http_code}\n" \ -X POST "https://emic.sh/api/v1/guidance" \ -H "authorization: Bearer $EMIC_API_KEY" \ -H "content-type: application/json" \ -d '{"situation":"install verification probe. Safe to ignore."}' Expected output: feedback HTTP 201 then a JSON body with a "guidance" field and guidance HTTP 200 These probes verify connectivity only. Also inspect the final tool catalog and confirm that submit_agent_feedback and get_unblocked are visible to the agent. Then start the product through its normal runtime and call the installed submit_agent_feedback tool through that runtime with tool_or_endpoint set to install_check. The workspace owner must confirm that exact record arrived. Do not declare the integration complete from curl alone. - HTTP 401 → EMIC_API_KEY missing or wrong. Check .env. Do not invent a key. - HTTP 422 → request body is malformed (a required field is missing). - Any other 4xx/5xx → STOP and report it. A non-201/200 is NOT "working"; the install is wrong. ``` --- ## API reference ### POST /api/v1/feedback Files one piece of agent feedback into the user's Emic workspace — a bug, friction, a feature request, a docs or schema issue, an awkward workflow, or what worked. (`POST /api/v1/report` is an accepted alias.) Reports carry a source tier (`source` field, default `agent_authored`): `agent_authored` (an agent chose to file), `agent_solicited` (filed as the byproduct of a get_unblocked help call), plus `server_observed` and `runtime_observed` for separately configured, permissioned studies. The canonical install creates only the two direct tools. It does not add observation, session capture, or wrappers around existing calls. Observed reports should set `metadata.heuristic` to the pattern name and must never be presented as the agent's own words. ### POST /api/v1/guidance The help endpoint behind the `get_unblocked` tool. Send an agent's situation; returns the maintainers' best known workaround (`{ guidance, matched, workaround_id?, filed }`) and files the situation as an `agent_solicited` report. Same Bearer auth. Body: `{ situation (required), tool_or_endpoint?, error_message?, what_would_unblock_you?, model?, channel? }`. Do not set `channel` in the default direct-tool install. The `inline_error` value is reserved for a separately consented observation integration. ### Optional: POST /api/v1/sessions (off by default) A team may explicitly choose a bounded session study, define its consent and data boundary, and post one compact study record per agent episode. This endpoint is not used by the canonical install and must not be silently enabled. Emic runs one LLM pass over the submitted call sequence and files one inferred experience report as `server_observed`, with `metadata.heuristic = "session_story"` and `llm_derived = true`. Body: `{ serverName, session: { calls: [{tool, args?, ok, err?}], duration_s? } }`. Same Bearer auth. Fewer than 2 calls, or a thin sequence, is acknowledged and skipped. If the story engine is not configured server-side, the endpoint acknowledges with `{ story: false }` and nothing in your install breaks. ### Optional runtime studies (agent runners only) People who run agents may separately configure permissioned runtime studies for friction that never reaches a provider. This is not part of the canonical install. It requires an explicit operator decision and a documented data boundary before any hook or session capture is enabled. `POST /api/v1/runtime/claude-code` accepts raw Claude Code hook events for that separately configured mode; results are labeled `runtime_observed`, not agent-authored. **URL:** `https://emic.sh/api/v1/feedback` **Headers:** | Header | Value | |-----------------|--------------------------------| | `content-type` | `application/json` | | `authorization` | `Bearer ` | **Request body (JSON):** Only two fields are required: `feedback_type` and `tool_or_endpoint`. Everything else is optional — send whatever fits the kind of feedback you're filing. | Field | Type | Required | Notes | |-------------------------|----------|----------|----------------------------------------------------------------| | `feedback_type` | string | yes | One of: `support_ticket`, `bug_or_failure`, `friction`, `feature_request`, `docs_feedback`, `schema_feedback`, `workflow_feedback`, `positive_feedback`. | | `tool_or_endpoint` | string | yes | The exact tool name or API endpoint this feedback is about. | | `outcome` | string | no | `success` / `partial_success` / `failure` — friction can ride on a success. | | `agent_goal` | string | no | The higher-level goal the agent was pursuing. | | `task` | string | no | The specific task this tool was meant to do. | | `what_worked` | string | no | What worked well — praise is signal too. | | `what_was_confusing` | string | no | What was ambiguous or surprising. | | `what_was_missing` | string | no | A capability/field/mode the agent needed but lacked. | | `what_would_improve_it` | string | no | The concrete change that would make it better. | | `expected_behavior` | string | no | For a bug/failure: what was expected. | | `actual_behavior` | string | no | For a bug/failure: what happened instead. | | `error_message` | string | no | The error/status returned, verbatim. | | `attempted_calls` | string[] | no | The calls tried, in order — a minimal replay. | | `failure_type` | string | no | For a failure: the granular kind — one of `missing_capability`, `confusing_schema`, `bad_tool_description`, `incomplete_response`, `incorrect_response`, `unhelpful_error`, `authentication_problem`, `pagination_or_truncation`, `rate_limit_or_timeout`, `docs_mismatch`, `wrong_tool_selected`, `other`. | | `satisfaction_score` | integer | no | 1 (bad) to 5 (great). | | `severity` | string | no | `blocking` / `high` / `medium` / `low` — derived if omitted. | | `serverName` | string | rec. | The product name (derive from package.json "name"). | | `surface` | string | rec. | One of: `mcp`, `rest`, `vercel-ai`, `langchain`, `other`. | | `agent` | object | no | Self-reported identity: `{ model }`. | A title is generated server-side from the feedback — you do not send one. **Status codes:** | Code | Meaning | |------|--------------------------------------------------------------------------| | 201 | **Success.** Feedback filed. Body contains `{ ok, id }`. | | 401 | Missing or invalid `Authorization: Bearer` header. | | 422 | Validation failed (a required field is missing). | | 429 | Rate-limited. | Success is **201**, not 200. Do NOT declare the install done if you see a 4xx. ### Example: a failure (support ticket) ```bash curl -X POST https://emic.sh/api/v1/feedback \ -H "authorization: Bearer agr_live_..." \ -H "content-type: application/json" \ -d '{ "serverName": "my-mcp-server", "surface": "mcp", "feedback_type": "support_ticket", "tool_or_endpoint": "create_invoice", "agent_goal": "bill a customer for a completed order", "task": "create an invoice for a customer identified by email", "outcome": "failure", "failure_type": "missing_capability", "expected_behavior": "resolve the email to a customer_id, then invoice", "actual_behavior": "create_invoice requires customer_id; no lookup tool exists", "error_message": "400 customer_id is required", "attempted_calls": ["create_invoice({email}) -> 400", "list_customers() -> not found"], "what_was_missing": "a get_customer_by_email lookup, or an email field on create_invoice", "what_would_improve_it": "add get_customer_by_email or document the lookup flow", "severity": "blocking", "agent": { "model": "claude-opus-4-8" } }' ``` ### Example: what worked (positive feedback) — friction on a success ```bash curl -X POST https://emic.sh/api/v1/feedback \ -H "authorization: Bearer agr_live_..." \ -H "content-type: application/json" \ -d '{ "serverName": "my-mcp-server", "surface": "mcp", "feedback_type": "positive_feedback", "tool_or_endpoint": "search_orders", "task": "find recent orders for a customer", "outcome": "success", "what_worked": "the cursor pagination and the date filters were exactly what I needed", "what_was_confusing": "the limit defaulted to 10 silently — I expected all results", "what_would_improve_it": "document the default limit in the tool description", "satisfaction_score": 4, "agent": { "model": "claude-opus-4-8" } }' ``` Expected: `HTTP 201` with body `{"ok":true,"id":""}`. --- ## Example handler (MCP server, TypeScript) ```ts // register next to your existing tools — does not wrap or modify them. server.registerTool( 'submit_agent_feedback', { title: 'Leave feedback about ', description: 'After you use this product, report your experience: a bug, friction on a call that still worked, a missing capability, confusing docs or schema, an awkward workflow, or simply what worked.', inputSchema: { // translate to zod / JSON Schema / your framework's format. // required: feedback_type, tool_or_endpoint. Everything else is optional. feedback_type: { enum: ['support_ticket', 'bug_or_failure', 'friction', 'feature_request', 'docs_feedback', 'schema_feedback', 'workflow_feedback', 'positive_feedback'] }, tool_or_endpoint: { type: 'string' }, outcome: { enum: ['success', 'partial_success', 'failure'] }, agent_goal: { type: 'string' }, task: { type: 'string' }, what_worked: { type: 'string' }, what_was_confusing: { type: 'string' }, what_was_missing: { type: 'string' }, what_would_improve_it:{ type: 'string' }, expected_behavior: { type: 'string' }, actual_behavior: { type: 'string' }, error_message: { type: 'string' }, attempted_calls: { type: 'array', items: { type: 'string' } }, failure_type: { enum: ['missing_capability', 'confusing_schema', 'bad_tool_description', 'incomplete_response', 'incorrect_response', 'unhelpful_error', 'authentication_problem', 'pagination_or_truncation', 'rate_limit_or_timeout', 'docs_mismatch', 'wrong_tool_selected', 'other'] }, satisfaction_score: { type: 'integer' }, severity: { enum: ['blocking', 'high', 'medium', 'low'] }, model: { type: 'string' }, }, }, async (args) => { const url = process.env.EMIC_URL || process.env.AGENTRATE_URL || 'https://emic.sh'; // fire-and-forget — never block the agent path: fetch(`${url}/api/v1/feedback`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.EMIC_API_KEY}`, }, body: JSON.stringify({ serverName: 'my-mcp-server', // derived from package.json surface: 'mcp', feedback_type: args.feedback_type, tool_or_endpoint: args.tool_or_endpoint, outcome: args.outcome, agent_goal: args.agent_goal, task: args.task, what_worked: args.what_worked, what_was_confusing: args.what_was_confusing, what_was_missing: args.what_was_missing, what_would_improve_it: args.what_would_improve_it, expected_behavior: args.expected_behavior, actual_behavior: args.actual_behavior, error_message: args.error_message, attempted_calls: args.attempted_calls, failure_type: args.failure_type, satisfaction_score: args.satisfaction_score, severity: args.severity, agent: { model: args.model }, }), }).catch(() => {}); return { content: [{ type: 'text', text: 'Feedback recorded — thanks.' }] }; }, ); ``` --- ## Example handler (Anthropic / OpenAI tool use) Define a function/tool named `submit_agent_feedback` with the schema above. When the model calls it, POST the arguments to `https://emic.sh/api/v1/feedback` exactly as in the MCP example — same body, set `surface` to `'other'` and `agent.model` to your model. ```ts async function handleSubmitAgentFeedback(args) { await fetch(`${process.env.EMIC_URL || process.env.AGENTRATE_URL || 'https://emic.sh'}/api/v1/feedback`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${process.env.EMIC_API_KEY}`, }, body: JSON.stringify({ serverName: 'my-agent-app', surface: 'other', feedback_type: args.feedback_type, tool_or_endpoint: args.tool_or_endpoint, outcome: args.outcome, agent_goal: args.agent_goal, task: args.task, what_worked: args.what_worked, what_was_confusing: args.what_was_confusing, what_was_missing: args.what_was_missing, what_would_improve_it: args.what_would_improve_it, expected_behavior: args.expected_behavior, actual_behavior: args.actual_behavior, error_message: args.error_message, attempted_calls: args.attempted_calls, failure_type: args.failure_type, satisfaction_score: args.satisfaction_score, severity: args.severity, agent: { model: args.model }, }), }).catch(() => {}); } ``` --- ## Verification (always run after install) ```bash # source .env into the shell so $EMIC_API_KEY is populated: set -a; . ./.env; set +a curl -sS -o /dev/null -w "HTTP %{http_code}\n" \ -X POST "https://emic.sh/api/v1/feedback" \ -H "authorization: Bearer $EMIC_API_KEY" \ -H "content-type: application/json" \ -d '{"serverName":"my-product","surface":"mcp","feedback_type":"positive_feedback","tool_or_endpoint":"install_check","outcome":"success","what_worked":"Install verification — safe to ignore."}' ``` Expected: `HTTP 201`. Any other code = install is wrong, STOP and report. --- ## More - Agent user research use cases: https://emic.sh/use-cases - Agent user research guides: https://emic.sh/guides - Collect feedback from AI agents: https://emic.sh/guides/collect-feedback-from-ai-agents - Agent feedback provenance: https://emic.sh/guides/agent-feedback-provenance - Desktop Commander field report: https://emic.sh/research/desktop-commander - GitMCP field report: https://emic.sh/research/gitmcp - Inbox (humans read feedback here): https://emic.sh/dashboard - Create or rotate ingest keys: https://emic.sh/dashboard/settings - Install page (interactive setup): https://emic.sh/dashboard/connect - Pricing (free for beta): https://emic.sh/pricing