# AgentLedger Per-agent spend management — the Datadog for agent spending. Track spend across x402/MPP/API-key rails, budget caps, anomaly alerts, audit trails. Machine-readable schema: GET /openapi.json (OpenAPI 3) · MCP manifest: GET /server.json Human/agent status page: GET /status (live health, version, uptime, counters) Privacy: GET /privacy · Terms: GET /terms Data handling: cost metadata only — agent id, rail, service label, amount, token counts, model, timestamp. There is no field for prompt or response content; none is collected and none is stored. Prompts and responses are never used to train models, and your data is never sold. ## Ownership (a workspace_key claims; an agent_secret writes) Claiming a NEW agent_id requires a workspace_key in the body of the first write (POST /v1/track or /v1/budget). Get one self-serve with no human at all by paying via POST /v1/billing/x402 (the paying wallet becomes the workspace identity), or by opening GET /start — no signup, no login, no card. Missing or invalid key on a new claim gets 401 workspace_key_required. That first write mints an `agent_secret` and returns it once, e.g. {"agent_secret": "...", "_note": "..."}. Save it — every later write to that same agent_id must include it in the body as "agent_secret" (the workspace_key is never needed again for that agent), or the request is rejected with 401. Reads /v1/report, /v1/tokens, and /v1/alerts all require an X-Agent-Secret or X-Workspace-Key header (either credential proving access to that agent_id) — missing/wrong gets 401. There is no unauthenticated read path, on REST or MCP. A free workspace is capped at 3 agents; a 4th new agent_id gets 402 until upgrading ($19/mo, unlimited agents). The cap is per workspace, not site-wide. Amounts per entry are capped at $100,000 and must be >= 0. Setting a budget makes it enforced going forward: a track() entry that would cross the monthly/daily cap is rejected with 402, not just logged. Dollar caps (monthly_cents/daily_cents) and token caps (monthly_tokens/ daily_tokens) are independent: dollar caps only cover non-"tokens" rails; token caps only cover rail="tokens" bookkeeping rows. A token-metered agent (flat-rate billing, no dollar amount per call) needs a token cap set — a dollar cap alone does not protect it. See ledger_api_docs("budget"). ## Validation rules (enforced on every write) - agent_id: 1-64 chars, letters/digits/./_/-, must start alphanumeric. Path traversal ("../", "/", leading dot) is rejected with 422. - rail: exactly one of "mpp", "x402", "api_key", "manual" (other values → 422). The special rail value "tokens" is reserved for internal token-burn bookkeeping rows: amount_cents must be 0. - amount_cents: integer 0-10000000 ($0-$100,000 per entry). - monthly_cents / daily_cents (budget): integers 0-10000000. ## Endpoints GET /health — liveness POST /v1/track — record a spend entry (mints/verifies agent_secret) body: {"agent_id": str, "rail": str, "amount_cents": int (0-10000000) — OPTIONAL: omit it and send tokens_in/tokens_out + model, and the amount is computed from the price table (GET /v1/pricing). An unpriced model is refused with 422 model_not_priced rather than recorded as zero. "service": str — OPTIONAL when auto-pricing; defaults to the model's provider "tokens_in": int (optional), "tokens_out": int (optional), "model": str (optional), "workspace_key": str (required to CLAIM a new agent_id), "agent_secret": str (required after the first call for this agent_id)} POST /v1/budget — set budget caps (mints/verifies agent_secret); once set, track() blocks entries that would cross the cap body: {"agent_id": str, "monthly_cents": int (0-10000000), "daily_cents": int (optional, 0-10000000), "monthly_tokens": int (optional, token-burn cap), "daily_tokens": int (optional, token-burn cap), "workspace_key": str (required to CLAIM a new agent_id), "agent_secret": str (required after the first call for this agent_id)} GET /v1/report/{agent_id} — spend report (query: days=30) — requires X-Agent-Secret or X-Workspace-Key POST /v1/report/{agent_id}/share — mint a read-only, EXPIRING link to the human report page (agent_secret OR workspace_key; default 7 days, max 90) POST /v1/report/{agent_id}/share/revoke — kill every outstanding share link for this agent GET /v1/report/{agent_id}/html?t= — the shared page itself. A BROWSER CANNOT SEND A HEADER, which is why this exists: open this URL directly, no credential, no curl. Bad/expired/revoked token returns a styled HTML error page, never raw JSON. The token reads that ONE agent_id only — it cannot write, rotate, or read others. GET /v1/tokens/{agent_id} — token burn report: in/out totals + by model (query: days=30) — requires X-Agent-Secret or X-Workspace-Key GET /v1/alerts/{agent_id} — alerts for agent — requires X-Agent-Secret or X-Workspace-Key POST /v1/agents/{agent_id}/rotate-secret — RECOVER a lost agent_secret: mints a new one, kills the old (workspace_key ONLY — never the agent_secret itself; 404 if unclaimed, 403 if the agent belongs to another workspace) POST /v1/agents/{agent_id}/revoke-secret — invalidate an agent's secret, keep its ledger (workspace_key ONLY; the agent_id stays claimed, so no other workspace can take it over; rotate back in when you want it writing) POST /v1/webhooks — register an alert destination for this workspace (X-Workspace-Key; http(s) URL only — no email rail) GET /v1/webhooks — list this workspace's destinations GET /v1/webhooks/deliveries — delivery receipts, successes AND failures DELETE /v1/webhooks/{id} — remove one destination Events: alert.raised, budget.warning (80%), budget.exceeded, anomaly.detected. Omit events to get all. Payloads carry cost metadata only — never a secret, prompt, or response. GET /v1/agents — owner-only: full cross-tenant listing (requires X-Al-Admin header) GET /stats — usage counters POST /v1/billing/x402 — self-serve workspace minting for an agent with a wallet (X-PAYMENT header; the paying wallet IS the identity) GET /start — get a workspace (no signup, no login); POST /start mints one and shows the key once ## Getting started (the shortest path) pip install "agent-ledger[wrapper]" # or: uvx --from git+https://github.com/entradox/agent-ledger agent-ledger init agent-ledger init --agent my-agent # mints a workspace, claims an agent, writes .env then, in your code: from openai import OpenAI import agentledger client = agentledger.wrap(OpenAI(api_key=OPENAI_KEY), agent_id="my-agent", agent_secret="") Every call now goes through the proxy: metered, and refused before the provider is contacted if it would cross a cap. Streaming, tool calls and retries are unchanged — wrap() only repoints the base URL and adds two headers; it does not patch or subclass the SDK. Anthropic's client works the same way. `agent-ledger share --agent-id my-agent --agent-secret ` prints a read-only link anyone can open in a browser. ## Proxy (enforcement — a cap that stops money, not just a record) POST /proxy/{provider}/{path} — provider = openai, anthropic, or ANY provider listed in providers.json (deepseek and moonshot ship as examples). Adding a vendor is a config edit, not a code change — spend must be meterable regardless of which vendor an agent calls. {path} is the provider's own path, e.g. /proxy/openai/v1/chat/completions headers: X-AL-Agent: (who gets billed) X-AL-Secret: (proves you may write for it) Authorization / x-api-key: YOUR provider credential — forwarded, NEVER stored flow: identify -> ESTIMATE the call's max cost -> if it would cross a cap, 402 to YOU and the provider is never contacted -> otherwise forward -> meter from the provider's real `usage` tokens PASS-THROUGH: AgentLedger holds no provider key. A caller can bypass the proxy entirely, and traffic that does is not enforced. Nothing here claims otherwise. An UNPRICED model is never blocked: the call passes through and an alert fires, because a silent zero would read as "this agent spends nothing". PRICING IS KEYED BY MODEL, not by provider — the same model costs the same through any carrier, so a caller cannot route around its own price by switching endpoints. Where a provider reports cached input (DeepSeek: prompt_cache_hit_tokens), the cache-hit rate is used; ignoring it would overstate a cache-heavy workload by roughly 50x. GET /v1/pricing — the price table in use + provenance (open read). Unverified entries are placeholders: check them against your provider. ## MCP Registry: io.github.entradox/agent-ledger Remote: https://aiagentscity.com/mcp/ Tools exposed at POST /mcp/: ledger_track — record a spend entry (workspace_key to claim, agent_secret after) ledger_set_budget — set a budget cap (workspace_key to claim, agent_secret after) ledger_report — get a spend report (agent_secret or workspace_key param) ledger_alerts — get alerts for an agent (agent_secret or workspace_key param) ledger_rotate_secret — recover a lost agent_secret (workspace_key param; old secret dies at once) ledger_revoke_secret — invalidate an agent's secret without deleting its history (workspace_key param) ledger_list_agents — owner-only (admin_secret param) ledger_api_docs — self-serve docs by topic: quickstart|mcp|rest|budget|errors|idempotency|metering|all (open read) ledger_examples — runnable recipe by pattern: python_tracking|budget_enforcement|weekly_report|retry_safe_writes (open read) Note: MCP and REST are credential-equivalent. ledger_report/ledger_alerts take agent_secret/workspace_key parameters and enforce the same access rule as GET /v1/report and GET /v1/alerts — there is no unauthenticated read path on either surface. Only the meta-doc tools (ledger_api_docs, ledger_examples) are open, and they expose no agent data. Every /v1/* REST write (POST /v1/track, POST /v1/budget) must send AL-API-Version: 2026-09-01 — missing/invalid values are rejected with 400. The /mcp/ endpoint itself does not require this header (MCP tool calls are not version-gated). POST /v1/track and POST /v1/budget accept an optional Idempotency-Key header (<=255 chars) for at-most-once retries. Pricing is as described above (free tier = 3 agents per workspace; Pro = $19/mo, unlimited agents). Contact: entradox@icloud.com