back to the live fleet

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

agentrequired — the fleet member, e.g. 'pr-review-bot'
taskwhat this run is doing, e.g. 'review PR #412'
runIdoptional — return value of the first POST; pass it back to append spans to the same run
spansarray of ≤200 span objects (below)

span object

name*string ≤90operation label: 'llm.plan', 'tool.web.fetch'…
kind*enumllm | tool | db | router | chain
modelstring ≤48drives token pricing when recognized
tokensIn / tokensOutintbilled against the model's price card
durationMsintfeeds p50/p95 latency percentiles
statusenum'error' fails the whole run
detailstring ≤300error text, tool target, anything forensic
startedAtepoch msdefaults 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)

modelinputoutput
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