Get started
Introduction
Record every LLM and agent run as one trace, keep it as long as you like in a Firebase project you own, and read it back from your agents over MCP.
Get started
Everything below talks to this deployment at https://tracing.art3m1s.me. All of it authenticates with one project API key (ft_live_<keyId>_<secret>), created in the dashboard under Project settings → API keys and shown exactly once. There is no keyless mode: FireTrace stores your traces in your own database, so it never accepts anonymous writes.
Connect an agent over MCP
Set up FireTrace MCP
See all setup options →Pick a client, paste the snippet, and your agent can list traces, walk span trees, record its own runs and leave scores. Tools are registered per key scope, so a read-only key never exposes a write tool.
claude mcp add --transport http firetrace https://tracing.art3m1s.me/api/mcp \
--header "Authorization: Bearer ft_live_..."{
"mcpServers": {
"firetrace": {
"type": "http",
"url": "https://tracing.art3m1s.me/api/mcp",
"headers": { "Authorization": "Bearer ft_live_..." }
}
}
}{
"mcpServers": {
"firetrace": {
"command": "npx",
"args": ["-y", "@firetrace/mcp"],
"env": {
"FIRETRACE_ENDPOINT": "https://tracing.art3m1s.me",
"FIRETRACE_API_KEY": "ft_live_..."
}
}
}
}import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
const transport = new StreamableHTTPClientTransport(new URL("https://tracing.art3m1s.me/api/mcp"), {
requestInit: { headers: { Authorization: `Bearer ${process.env.FIRETRACE_API_KEY}` } },
});
const client = new Client({ name: "my-agent", version: "1.0.0" });
await client.connect(transport);
const failures = await client.callTool({
name: "list_traces",
arguments: { status: "error", limit: 10 },
});Using another MCP client? Point it at https://tracing.art3m1s.me/api/mcp with an Authorization: Bearer header.
Give an investigating agent a read-only key with an expiry, then ask it something like “list the last 10 error traces and tell me what broke.” Full tool reference in MCP.
Send your first trace
One request stores one complete trace with all of its spans; a long agent run can instead start the trace first and stream spans as they finish. No agent to install. Any language that can POST JSON is a supported client.
curl -s -X POST https://tracing.art3m1s.me/api/v1/traces \
-H "Authorization: Bearer $FIRETRACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"schemaVersion": 1,
"trace": {
"id": "42f38ac8295345a7a12c4e3f60d6da23",
"name": "answer-question",
"status": "ok",
"startedAt": "2026-09-02T19:01:02.120Z",
"endedAt": "2026-09-02T19:01:04.812Z",
"model": "example-model",
"input": {
"prompt": "Explain vector search in two sentences."
},
"output": {
"text": "Vector search finds items whose embeddings are nearest to a query embedding."
},
"usage": {
"inputTokens": 412,
"outputTokens": 96,
"totalTokens": 508
},
"tags": [],
"metadata": {},
"spans": [
{
"id": "00f067aa0ba902b7",
"parentSpanId": null,
"name": "generate-text",
"kind": "llm",
"status": "ok",
"startedAt": "2026-09-02T19:01:02.145Z",
"endedAt": "2026-09-02T19:01:04.780Z",
"model": "example-model",
"attributes": {},
"events": [],
"usage": {}
}
]
}
}'import { FireTrace } from "@firetrace/sdk";
const client = new FireTrace({
endpoint: process.env.FIRETRACE_ENDPOINT!, // "https://tracing.art3m1s.me"
apiKey: process.env.FIRETRACE_API_KEY!, // "ft_live_<keyId>_<secret>"
onError: (err) => console.warn("firetrace:", err.code, err.message),
});
const trace = client.startTrace("answer-question", {
model: "example-model",
input: { prompt },
});
const llm = trace.startSpan("generate-text", { kind: "llm", model: "example-model" });
const result = await callModel();
llm.end({ status: "ok", output: { text: result.text }, usage: result.usage });
const sent = await trace.end({ status: "ok", output: { text: result.text } });
if (sent.ok) console.log(sent.response.traceId, sent.response.duplicate);const body = {
schemaVersion: 1,
trace: {
id: crypto.randomUUID().replaceAll("-", ""), // 32 hex characters
name: "answer-question",
status: "ok",
startedAt: startedAt.toISOString(),
endedAt: new Date().toISOString(),
model: "example-model",
input: { prompt },
output: { text },
spans: [],
},
};
const res = await fetch("https://tracing.art3m1s.me/api/v1/traces", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FIRETRACE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
if (!res.ok) console.warn("firetrace:", res.status, await res.text());Response · trace stored
{
"ok": true,
"traceId": "42f38ac8295345a7a12c4e3f60d6da23",
"projectId": "5eedc0ffee5eedc0ffee5eed",
"spanCount": 1,
"duplicate": false,
"running": false,
"requestId": "0f1e2d3c4b5a6978"
}The trace is then visible at /projects/{projectId}/traces/{traceId}. Sending the same body again is a 200 with "duplicate": true rather than a second trace, so retrying after a timeout is safe. See the ingestion API for the full field list, limits and error codes.
Other ways to get started
Fastest
Deploy with an AI agent
Paste one prompt into your coding agent. It creates the Firebase project, sets the environment variables, deploys to Vercel and sends a smoke-test trace.
Step by step
Set up Firebase by hand
Create the project and database, enable sign-in providers, and mint the Admin credential the server uses.
Step by step
Deploy to Vercel
Import the repository, set the environment variables, authorize the domain, and run the smoke test.
No deploy
Try it on this deployment
Sign in with any verified Google or email account and record up to 5 traces against this instance before you deploy your own.
What can FireTrace do?
Record
A POST stores a run as a tree of spans with inputs, outputs, tokens, cost, errors and tags, whole or streamed as it happens.
Read
Filter and page through traces from the dashboard, the REST API or an agent; open one as a span tree, waterfall and inspector.
Judge
Attach typed scores by hand, from an agent, or from an LLM-as-judge evaluator you define.
Why FireTrace?
- Yours, not rented: the app runs on your Vercel account against a Firebase project you own. FireTrace has no servers of its own and no access to your data.
- No expiry, ever: there is no TTL field, no
expireAt, no cleanup job and no age-based deletion. Only an explicit owner action deletes a trace. Capacity is bounded by your Firebase plan, not by a retention window. - One POST per run, or streamed: a complete trace with up to 200 nested spans in a single request, deduplicated by a hash of the normalized body; or a start, span batches and an end, so a crash still leaves what happened.
- Agent-native: the same data is available over MCP, over a REST API described by an OpenAPI 3.1 document, and in the dashboard.
- Environments come from the key: the server stamps
production,previewor whatever slug the key carries onto every trace, so your application never sends an environment field and cannot mislabel one.
Record
POST /api/v1/traces with scope traces:write. Bodies are strict: an unknown key at any level is a 400 invalid_trace, so a typo can never be silently dropped. Trace ids are 32 hex characters, span ids 16 — the OpenTelemetry widths. Beyond the quickstart above, a real run is a tree: up to 200 spans, each with a parentSpanId, its own timings, tokens and cost, and up to 50 events.
There is no dedicated error field. A failing step sets status: "error" and records error.type and error.message in its attributes; the dashboard's Error section reads them from there. Full rules in the ingestion API.
const trace = client.startTrace("answer-question", { input: { prompt } });
const agent = trace.startSpan("answer-question", { kind: "agent" });
agent.addEvent("plan.ready", { steps: 2 });
const tool = agent.startSpan("lookup", { kind: "tool", input: { url } });
tool.end({ status: "ok", output: { status: 200 } });
const llm = agent.startSpan("generate-text", {
kind: "llm",
provider: "example-provider",
model: "example-model",
input: { messages },
});
llm.end({ status: "ok", output: { text }, usage, costUsd });
agent.end({ status: "ok" });
await trace.end({ status: "ok", output: { text }, usage });{
"id": "b7ad6b7169203331",
"parentSpanId": "00f067aa0ba902b7",
"name": "lookup-example",
"kind": "tool",
"status": "error",
"startedAt": "2026-09-02T19:01:03.050Z",
"endedAt": "2026-09-02T19:01:03.600Z",
"input": {
"tool": "docs.fetch",
"url": "https://example.com/vector-search"
},
"attributes": {
"error.type": "HttpError",
"error.message": "HTTP 429 Too Many Requests after 2 retries"
},
"events": [
{
"name": "retry",
"timestamp": "2026-09-02T19:01:03.220Z",
"attributes": {
"attempt": 1
}
},
{
"name": "retry",
"timestamp": "2026-09-02T19:01:03.420Z",
"attributes": {
"attempt": 2
}
}
],
"usage": {}
}Read
GET /api/v1/traces with scope traces:read returns a newest-first page with cursor pagination. Filters (status, model, name, tag, environment, sessionId, userId, from, to) combine with AND, and sort can be newest, slowest or costliest. The query string is strict too, so a misspelled filter is a 400 rather than an accidental unfiltered list.
curl -s "https://tracing.art3m1s.me/api/v1/traces?status=error&limit=5" \
-H "Authorization: Bearer $FIRETRACE_API_KEY"import { FireTraceApi } from "@firetrace/sdk";
const api = new FireTraceApi({
endpoint: "https://tracing.art3m1s.me",
apiKey: process.env.FIRETRACE_API_KEY!,
});
const page = await api.listTraces({ status: "error", limit: 20 });
const detail = await api.getTrace(page.traces[0].id); // trace, spans and scores
for await (const trace of api.iterateTraces({ model: "example-model" })) console.log(trace.id);Using the firetrace MCP server, list the last 10 error traces in this
project. For the slowest one, find the failing span with find_spans, read
the surrounding input and output with get_trace, and tell me what broke.
Then record your verdict on that trace with add_score.Response · one trace from a page
{
"traces": [
{
"id": "42f38ac8295345a7a12c4e3f60d6da23",
"name": "answer-question",
"status": "ok",
"environment": "production",
"startedAt": "2026-09-02T19:01:02.120Z",
"endedAt": "2026-09-02T19:01:04.812Z",
"durationMs": 2692,
"provider": null,
"model": "example-model",
"sessionId": "session-123",
"userId": null,
"tags": [],
"usage": { "inputTokens": 120, "outputTokens": 84, "totalTokens": 204 },
"costUsd": null,
"spanCount": 5,
"errorCount": 1,
"estimatedBytes": 4310,
"ingestedAt": "2026-09-02T19:01:05.004Z",
"scores": {
"helpful": {
"scoreId": "9c1e7a2b3d4f5061",
"dataType": "boolean",
"value": true,
"evaluatorId": null
}
}
}
],
"nextCursor": "eyJz...",
"prevCursor": null,
"pageSize": 50
}Judge
A score is a typed judgement attached to a trace after the run: a thumbs rating, a reviewer's verdict, an evaluator's result. Scores are indexed and append-only, they show up on the trace page and in the trace list, and every trace carries the newest score per name. Configure the optional LLM judge and you can run an evaluator over one trace or over a filtered list — see scores and evaluators.
curl -s -X POST https://tracing.art3m1s.me/api/v1/traces/$TRACE_ID/scores \
-H "Authorization: Bearer $FIRETRACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"helpful","dataType":"boolean","value":true,"comment":"answered the question"}'await api.addScore(traceId, {
name: "helpful",
dataType: "boolean",
value: true,
comment: "answered the question",
});
// Every "helpful" score across the project, newest first.
const helpful = await api.listScores({ name: "helpful", limit: 100 });Response · score stored
{
"ok": true,
"score": {
"id": "9c1e7a2b3d4f5061",
"traceId": "42f38ac8295345a7a12c4e3f60d6da23",
"spanId": null,
"name": "helpful",
"dataType": "boolean",
"value": true,
"comment": "answered the question",
"source": "api",
"evaluatorId": null,
"runId": null,
"createdAt": "2026-09-03T10:12:00.000Z"
},
"requestId": "0f1e2d3c4b5a6978"
}More capabilities
Environments
Label a key production, preview or your own slug; the server stamps it on every trace and the dashboard filters by it.
Scoped keys
traces:write, traces:read and traces:delete, with optional expiry and one-transaction rotation.
Metadata patch
PATCH merges free-form facts into a stored trace without touching its body hash, for judgements that only arrive later.
Idempotent ingest
The body hash is computed after normalization, so a retry that differs only in key order or timestamp notation is a duplicate, not a conflict.
MCP tools
list_traces, get_trace, find_spans, record_trace, add_score and more, registered per key scope.
LLM-as-judge evaluators
Point FireTrace at any OpenAI-compatible endpoint, define a judge from a template, and run it over a trace or a filtered list.
Security model
Deny-all Firestore rules, Admin-SDK-only access, HMAC key digests, origin checks and a deployment checklist.
Operational notes
What each call costs in Firestore reads, what is cacheable, and how cold starts show up in latency.
Resources
API reference
Every key-authenticated endpoint, with scopes, filters, responses and error codes.
Ingestion API
The wire format in full: field rules, limits, normalization, idempotency, error conventions.
TypeScript SDK
@firetrace/sdk — no runtime dependencies, never throws into your application unless you ask it to.
Source on GitHub
The whole application, MIT licensed. Fork it, audit it, run it yourself.
For AI agents: fetch /api/v1/openapi.json for this deployment's machine-readable contract, browse /api/v1 for the endpoint index, or connect an MCP client to POST /api/mcp with a bearer key. Every page in these docs has a Copy page as Markdown button for pasting into a model.