axon docs · v1
Ten lines to full observability.
axon ingests plain json over http. no sdk to install, no framework to adopt, no auth for the open tier. emit spans; axon rolls them into runs with cost, tokens, latency and failure state — and renders the fleet live at the root url.
quickstart
curl -X POST $AXON/api/axon/spans \
-H 'content-type: application/json' \
-d '{"agent":"my-agent","task":"triage ticket #1","spans":[
{"name":"llm.plan","kind":"llm","model":"gpt-5.2","tokensIn":1240,"tokensOut":310,"durationMs":840},
{"name":"tool.web.search","kind":"tool","durationMs":420},
{"name":"llm.answer","kind":"llm","model":"gpt-5.2","tokensIn":2200,"tokensOut":880,"durationMs":1300}
]}'
# → {"runId":"…","spans":3,"costMicro":16200}payload reference
top level
| agent | required — the fleet member, e.g. 'pr-review-bot' |
| task | what this run is doing, e.g. 'review PR #412' |
| runId | optional — return value of the first POST; pass it back to append spans to the same run |
| spans | array of ≤200 span objects (below) |
span object
| name* | string ≤90 | operation label: 'llm.plan', 'tool.web.fetch'… |
| kind* | enum | llm | tool | db | router | chain |
| model | string ≤48 | drives token pricing when recognized |
| tokensIn / tokensOut | int | billed against the model's price card |
| durationMs | int | feeds p50/p95 latency percentiles |
| status | enum | 'error' fails the whole run |
| detail | string ≤300 | error text, tool target, anything forensic |
| startedAt | epoch ms | defaults to server receive time |
append mode: pass runId to add spans to an existing run — totals sum, never overwrite. a single error span flips the run to failed.
the 24-line sdk
the reference wrapper is a fetch call. copy it, own it, delete our name from it — the contract is the endpoint.
// axon.ts — the whole SDK, 24 lines. no dependencies.
const AXON = process.env.AXON_INGEST_URL; // e.g. https://your-host/api/axon/spans
let runId: string | null = null;
export async function span({
agent, task, name, kind, model, tokensIn, tokensOut, durationMs, status, detail,
}: {
agent: string; task?: string; name: string;
kind: "llm" | "tool" | "db" | "router" | "chain";
model?: string; tokensIn?: number; tokensOut?: number;
durationMs?: number; status?: "ok" | "error"; detail?: string;
}) {
const res = await fetch(AXON!, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ agent, task, runId, spans: [{ name, kind, model, tokensIn, tokensOut, durationMs, status, detail }] }),
});
const data = await res.json();
if (!runId) runId = data.runId; // first span creates the run; the rest append
}
// tip: wrap your llm/tool calls:
// const out = await timed(() => openai.chat(...), d => span({agent:'my-bot', name:'llm.plan', kind:'llm', ...d}))price card (per 1m tokens)
| model | input | output |
|---|---|---|
| Claude Opusclaude-opus-4.6 | $15.00 | $75.00 |
| Claude Sonnetclaude-sonnet-4.5 | $3.00 | $15.00 |
| GPT-5.2gpt-5.2 | $1.25 | $10.00 |
| Gemini 3 Progemini-3-pro | $2.00 | $12.00 |
| Llama 4 (local)llama-4-scout-local | $0.02 | $0.05 |
unrecognized model? spans still trace — cost just shows $0. prices are editable constants.
honesty clause
the fleet on the demo deployment is seed data plus explicitly labeled simulated agents (marked simulated in every waterfall). point your own agents at /api/axon/spans and their spans appear beside them — under your agent name, never relabeled.
axon · mit-style freedom · postgres native · deployable on anything that runs next.js