Documentation

AgentLedger tracks what your AI agents actually do — every API call, email, ticket, and charge. Get set up in under 5 minutes.

Quick Start

1

Set up the database

Create a Supabase project and run the migration in the SQL Editor.

-- Paste the contents of supabase/migrations/001_initial_schema.sql\n-- into Supabase Dashboard > SQL Editor > New Query > Run
2

Deploy the dashboard

Deploy to Vercel with one click, or run locally:

git clone https://github.com/agentledger-co/agentledger.git
cd agentledger
cp .env.local.example .env.local
# Add your Supabase URL, anon key, and service role key
npm install
npm run dev
3

Install the SDK and start tracking

npm install agentledger
agent.ts
import { AgentLedger } from 'agentledger';

const ledger = new AgentLedger({
  apiKey: process.env.AGENTLEDGER_KEY,
});

// Wrap any agent action
const result = await ledger.track({
  agent: 'support-bot',
  service: 'slack',
  action: 'send_message',
}, async () => {
  return await slack.chat.postMessage({
    channel: '#support',
    text: 'Issue resolved!'
  });
});

Installation

npm install agentledger

The SDK has zero external dependencies and works in Node.js 18+. It also works in Bun and Deno.

Configuration

config.ts
const ledger = new AgentLedger({
  apiKey: 'al_...',              // Required. Get from dashboard.
  baseUrl: 'https://...',       // Your AgentLedger instance URL
  failOpen: true,               // If AgentLedger is down, actions proceed (default: true)
  timeout: 5000,                // API timeout in ms (default: 5000)
  onError: (err) => log(err),   // Optional error callback
});
OptionDefaultDescription
apiKeyrequiredYour API key (starts with al_)
baseUrlhttps://agentledger.coYour AgentLedger API endpoint
failOpentrueIf true, actions proceed when AgentLedger is unreachable
timeout5000API call timeout in milliseconds
onErrorundefinedCallback for communication errors

Fail-open by default. AgentLedger never blocks your agents from running unless you explicitly set failOpen: false. Even budget checks fail-open — if the API is unreachable, the action proceeds.

Core SDK

ledger.track(options, fn)

Wraps an async function with logging and pre-flight budget checks. This is the main method you'll use.

track.ts
try {
  const { result, allowed, durationMs, actionId } = await ledger.track({
    agent: 'support-bot',       // Agent name
    service: 'sendgrid',        // Service being called
    action: 'send_email',       // Action being performed
    costCents: 1,               // Optional: estimated cost in cents
    metadata: { to: 'user@' },  // Optional: custom key-value metadata
    input: { to, subject },     // Optional: captured as action input for debugging
    output: undefined,          // Optional: explicit output value to log
    captureOutput: true,        // Optional: auto-capture return value of fn
  }, async () => {
    return await sendEmail(to, subject, body);
  });
  console.log('Action logged:', actionId, 'took', durationMs, 'ms');
} catch (e) {
  // Thrown when: policy blocked the action, agent is paused/killed,
  // budget exceeded, or the wrapped fn itself threw
  console.error('Action failed:', e.message);
}
OptionTypeDescription
agentstringName of the agent performing the action (required)
servicestringService being called, e.g. "openai", "stripe" (required)
actionstringAction being performed, e.g. "send_email" (required)
costCentsnumberEstimated cost in cents
metadataRecord<string, unknown>Custom key-value pairs logged with the action
traceIdstringTrace ID to group related actions (see Traces)
inputanyInput data (e.g. prompt, request body) stored for debugging
outputanyExplicit output value to log. Overrides captureOutput
captureOutputbooleanAuto-capture fn return value as output (default: false)

ledger.check(options)

Pre-flight check without executing the action. Useful before expensive operations.

check.ts
const { allowed, blockReason, remainingBudget } = await ledger.check({
  agent: 'billing-agent',
  service: 'stripe',
  action: 'charge',
});

if (!allowed) {
  console.log('Blocked:', blockReason);
}

ledger.log(options)

Log an action manually when you want full control over timing.

log.ts
await ledger.log({
  agent: 'data-sync',
  service: 'postgres',
  action: 'bulk_insert',
  status: 'success',
  durationMs: 1523,
  costCents: 0,
});

Agent Controls

await ledger.pauseAgent('support-bot');  // Blocks all future actions
await ledger.resumeAgent('support-bot'); // Resumes the agent
await ledger.killAgent('rogue-bot');     // Permanently kills the agent

LangChain Integration

Drop-in callback handler that auto-tracks tool calls, LLM completions, and chain runs.

npm install agentledger langchain @langchain/core
langchain-example.ts
import { AgentLedger } from 'agentledger';
import { AgentLedgerCallbackHandler } from 'agentledger/integrations/langchain';

const ledger = new AgentLedger({ apiKey: 'al_...' });

const handler = new AgentLedgerCallbackHandler(ledger, {
  agent: 'research-bot',
  trackLLM: true,     // Track LLM calls with token usage (default: true)
  trackTools: true,    // Track tool invocations (default: true)
  trackChains: false,  // Track chain/agent runs (default: false)
  serviceMap: {
    'tavily_search': { service: 'tavily', action: 'search' },
    'calculator': { service: 'math', action: 'calculate' },
    'send_email': { service: 'sendgrid', action: 'send' },
  },
});

// Use with any LangChain component
const agent = createReactAgent({
  llm: new ChatOpenAI({ callbacks: [handler] }),
  tools,
});

await agent.invoke(
  { input: 'Research the latest AI news and email me a summary' },
  { callbacks: [handler] }
);

The serviceMap lets you control how LangChain tool names map to AgentLedger services. If a tool isn't in the map, its name is used as the service with "invoke" as the action.

OpenAI Agents Integration

Wrap tool handlers so every function call from OpenAI is tracked.

openai-example.ts
import { AgentLedger } from 'agentledger';
import { createToolExecutor } from 'agentledger/integrations/openai';

const ledger = new AgentLedger({ apiKey: 'al_...' });

// Define your tool handlers
const handlers = {
  send_email: async (args) => sendEmail(args.to, args.body),
  create_ticket: async (args) => createJiraTicket(args.title, args.desc),
  charge_card: async (args) => stripe.charges.create(args),
};

// Map tool names to services
const serviceMap = {
  send_email: { service: 'sendgrid', action: 'send' },
  create_ticket: { service: 'jira', action: 'create_issue' },
  charge_card: { service: 'stripe', action: 'charge' },
};

// Create the executor
const execute = createToolExecutor(ledger, 'my-agent', handlers, serviceMap);

// In your OpenAI agent loop
for (const toolCall of response.choices[0].message.tool_calls) {
  const result = await execute(
    toolCall.function.name,
    JSON.parse(toolCall.function.arguments)
  );
  // ... send result back to OpenAI
}

Wrap individual functions

import { withAgentLedger } from 'agentledger/integrations/openai';

// Wrap a single function — preserves original signature
const trackedSendEmail = withAgentLedger(ledger, {
  agent: 'email-bot',
  service: 'sendgrid',
  action: 'send_email',
}, sendEmail);

// Use exactly like the original
await trackedSendEmail(to, subject, body);

MCP Server Integration

One line to track every tool invocation in your MCP server.

mcp-example.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { AgentLedger } from 'agentledger';
import { wrapMCPServer } from 'agentledger/integrations/mcp';

const ledger = new AgentLedger({ apiKey: 'al_...' });
const server = new McpServer({ name: 'my-tools', version: '1.0.0' });

// Register tools as normal
server.tool('send_email', { to: z.string(), body: z.string() }, async (args) => {
  return await sendEmail(args.to, args.body);
});

server.tool('search_web', { query: z.string() }, async (args) => {
  return await tavily.search(args.query);
});

// One line — all tool calls are now logged to AgentLedger
wrapMCPServer(ledger, server, {
  agent: 'my-mcp-server',
  serviceMap: {
    send_email: { service: 'sendgrid' },
    search_web: { service: 'tavily', action: 'search' },
  },
});

Wrap individual tools

import { wrapMCPTool } from 'agentledger/integrations/mcp';

server.tool('send_email', schema, wrapMCPTool(ledger, {
  agent: 'my-server',
  service: 'sendgrid',
  action: 'send_email',
}, async (args) => {
  return await sendEmail(args.to, args.body);
}));

Express / Generic Integration

Express middleware

express-example.ts
import { agentLedgerMiddleware } from 'agentledger/integrations/express';

// Track specific routes
app.post('/api/send-email', agentLedgerMiddleware(ledger, {
  agent: 'email-bot',
  service: 'sendgrid',
  action: 'send_email',
}), emailHandler);

// Auto-detect from path
app.use('/api/agent', agentLedgerMiddleware(ledger, {
  agent: 'my-agent',
  autoDetect: true,
}));

Wrap any function

Works with any framework — no Express required.

import { trackFunction } from 'agentledger/integrations/express';

const trackedSendEmail = trackFunction(ledger, {
  agent: 'my-bot',
  service: 'sendgrid',
  action: 'send_email',
}, sendEmail);

// Same signature as the original function
await trackedSendEmail(to, subject, body);

REST API

All endpoints require an Authorization: Bearer al_... header.

MethodEndpointDescription
POST/api/v1/actionsLog an agent action
GET/api/v1/actionsList actions (paginated)
POST/api/v1/checkPre-flight budget/status check
GET/api/v1/statsDashboard summary stats
GET/api/v1/agents/:nameAgent details + recent actions
POST/api/v1/agents/:name/pausePause an agent
POST/api/v1/agents/:name/resumeResume a paused agent
POST/api/v1/agents/:name/killPermanently kill an agent
POST/api/v1/budgetsCreate or update a budget
GET/api/v1/alertsList anomaly alerts

Example: Log an action

curl -X POST https://your-instance.vercel.app/api/v1/actions \
  -H "Authorization: Bearer al_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "support-bot",
    "service": "slack",
    "action": "send_message",
    "status": "success",
    "cost_cents": 0,
    "duration_ms": 150,
    "metadata": { "channel": "#support" }
  }'

Error responses

// 401 Unauthorized — missing or invalid API key
{ "error": "Invalid API key" }

// 403 Forbidden — action blocked by policy
{ "error": "Action blocked", "reason": "Policy: Rate limit exceeded (15/10 in 3600s)" }

// 429 Too Many Requests — API rate limiting
{ "error": "Rate limit exceeded", "retryAfter": 30 }

Example: Pre-flight check

curl -X POST https://your-instance.vercel.app/api/v1/check \
  -H "Authorization: Bearer al_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "billing-agent",
    "service": "stripe",
    "action": "charge"
  }'

// Response:
// { "allowed": true, "remainingBudget": {} }
// or
// { "allowed": false, "blockReason": "daily cost budget exceeded ($50.00/$50.00)" }

Webhooks

Get real-time HTTP notifications when events occur. Webhooks are signed with HMAC-SHA256 so you can verify authenticity.

Events

EventFired When
action.loggedAny agent action is recorded
agent.pausedAn agent is paused
agent.killedAn agent is permanently killed
agent.resumedA paused agent is resumed
budget.exceededA budget limit is reached
budget.warningA budget crosses 75% usage
alert.createdAny anomaly alert is created
batch.loggedA batch of actions is logged

Create a webhook

curl -X POST https://your-instance.vercel.app/api/v1/webhooks \
  -H "Authorization: Bearer al_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhook",
    "events": ["budget.exceeded", "agent.killed"],
    "description": "Slack alerts"
  }'

// Response includes a signing secret (shown only once):
// { "id": "...", "secret": "whsec_...", "url": "...", "events": [...] }

Webhook payload

Every webhook delivery sends a JSON body with the event type, timestamp, and event-specific data:

{
  "event": "action.logged",
  "timestamp": "2026-03-31T14:22:00.000Z",
  "data": {
    "id": "act_8f3a2b1c",
    "agent_name": "support-bot",
    "service": "slack",
    "action": "send_message",
    "status": "success",
    "cost_cents": 0,
    "duration_ms": 150,
    "environment": "production",
    "metadata": { "channel": "#support" },
    "created_at": "2026-03-31T14:22:00.000Z"
  }
}

Verify signatures

webhook-handler.js
// Every webhook request includes X-AgentLedger-Signature header
const crypto = require('crypto');

app.post('/webhook', (req, res) => {
  const signature = req.headers['x-agentledger-signature'];
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');

  if (signature !== expected) {
    return res.status(401).send('Invalid signature');
  }

  // Process the event
  const { event, data, timestamp } = req.body;
  console.log(`Received ${event}:`, data);
  res.sendStatus(200);
});

Auto-disable. Webhooks are automatically disabled after 10 consecutive delivery failures. Re-enable them from the dashboard or API.

Notification Channels

Get alerted through Slack, Discord, or PagerDuty when budgets are exceeded, agents go rogue, or anomalies are detected. Configure multiple channels per org.

Supported channels

ChannelConfig RequiredUse Case
emailEmail addressGeneral notifications
slackWebhook URLTeam chat alerts
discordWebhook URLTeam chat alerts
pagerdutyRouting keyOn-call incident escalation

Slack

Create a Slack Incoming Webhook and pass the URL:

curl -X POST https://your-instance.vercel.app/api/v1/notifications \
  -H "Authorization: Bearer al_..." \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "slack",
    "config": {
      "webhook_url": "https://hooks.slack.com/services/T.../B.../xxx"
    },
    "events": ["budget.exceeded", "alert.created"]
  }'

Discord

Create a Discord channel webhook (Server Settings → Integrations → Webhooks) and pass the URL:

curl -X POST https://your-instance.vercel.app/api/v1/notifications \
  -H "Authorization: Bearer al_..." \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "discord",
    "config": {
      "webhook_url": "https://discord.com/api/webhooks/123456/abcdef..."
    },
    "events": ["budget.exceeded", "agent.killed"]
  }'

PagerDuty

Create a PagerDuty service integration (Events API v2) and pass the routing key. Alerts map to PagerDuty severity levels automatically:

curl -X POST https://your-instance.vercel.app/api/v1/notifications \
  -H "Authorization: Bearer al_..." \
  -H "Content-Type: application/json" \
  -d '{
    "channel": "pagerduty",
    "config": {
      "routing_key": "your-pagerduty-integration-key"
    },
    "events": ["budget.exceeded", "agent.killed", "alert.created"]
  }'

// PagerDuty severity mapping:
//   budget.exceeded, agent.killed → critical
//   alert.created               → warning
//   budget.warning              → info

List active channels

curl https://your-instance.vercel.app/api/v1/notifications \
  -H "Authorization: Bearer al_..."

// Returns all configured channels with their events and status

API Key Management

Create up to 5 active API keys per workspace. Rotate and revoke keys without downtime.

Create a new key

curl -X POST https://your-instance.vercel.app/api/v1/keys/create \
  -H "Authorization: Bearer al_current_key" \
  -H "Content-Type: application/json" \
  -d '{ "name": "production", "description": "Main production key" }'

// Response includes the full key (shown only once):
// { "id": "...", "key": "al_...", "name": "production" }

Rotate a key

Atomically revokes an old key and creates a new one with the same name.

curl -X POST https://your-instance.vercel.app/api/v1/keys/rotate \
  -H "Authorization: Bearer al_current_key" \
  -H "Content-Type: application/json" \
  -d '{ "keyId": "key-uuid-to-rotate" }'

Revoke a key

curl -X POST https://your-instance.vercel.app/api/v1/keys/revoke \
  -H "Authorization: Bearer al_current_key" \
  -H "Content-Type: application/json" \
  -d '{ "keyId": "key-uuid-to-revoke" }'

Safety. You cannot revoke the key you're currently using to authenticate. This prevents accidental lockouts.

Dashboard

The dashboard provides a real-time view of all your agent activity.

Overview — total actions, costs, active agents, error rate, 24h activity chart, and service breakdown.

Actions — searchable, filterable feed of every action with agent, service, status, duration, and cost.

Agents — all registered agents with status, action counts, costs, and pause/kill controls.

Budgets — create and manage daily/weekly/monthly budgets per agent.

Alerts — anomaly alerts for budget exceeded, unusual activity spikes, and agent kills.

Budgets & Alerts

Set spending and action limits per agent. When a budget is exceeded, all future actions are blocked until the budget resets.

// Create a budget via the API
curl -X POST https://your-instance.vercel.app/api/v1/budgets \
  -H "Authorization: Bearer al_..." \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "billing-agent",
    "period": "daily",
    "max_actions": 1000,
    "max_cost_cents": 5000
  }'

Budget counters reset automatically:

PeriodResets At
dailyMidnight UTC
weeklyMonday midnight UTC
monthly1st of the month midnight UTC

Automatic budget resets require enabling pg_cron in your Supabase project. See the migration file for the cron schedule SQL.

See also: Policy Engine for rate limiting and cost-per-action caps.

Environments

Separate agent activity across dev, staging, and production. Each environment has its own action log, budgets, and alerts. The default is production when not specified.

SDK (TypeScript)

config.ts
const ledger = new AgentLedger({
  apiKey: 'al_...',
  environment: 'staging',  // 'production' | 'staging' | 'development' | any string
});

SDK (Python)

config.py
from agentledger import AgentLedger, AgentLedgerConfig

ledger = AgentLedger(AgentLedgerConfig(api_key="al_...", environment="staging"))

REST API

All endpoints accept an environment query parameter:

curl https://your-instance.vercel.app/api/v1/actions?environment=staging \
  -H "Authorization: Bearer al_..."

Dashboard. Use the environment selector in the header to switch between environments. All charts, tables, and alerts filter to the selected environment.

Traces

Group related actions into a single trace to see the full lifecycle of an agent task. Attach a traceId to every action in a workflow.

Generate a trace ID

Trace IDs use the format tr_{base36_timestamp}_{random_8chars} (e.g. tr_lq8k2m1_a7f3b9x2).

trace-example.ts
import { AgentLedger } from 'agentledger';

const traceId = AgentLedger.traceId(); // e.g. "tr_lq8k2m1_a7f3b9x2"

await ledger.track({
  agent: 'research-bot',
  service: 'tavily',
  action: 'search',
  traceId,
}, async () => {
  return await tavily.search(query);
});

await ledger.track({
  agent: 'research-bot',
  service: 'openai',
  action: 'summarize',
  traceId,  // same traceId links these actions together
}, async () => {
  return await openai.chat.completions.create({ ... });
});

Retrieve a trace

curl https://your-instance.vercel.app/api/v1/traces/tr_lq8k2m1_a7f3b9x2 \
  -H "Authorization: Bearer al_..."

# Response:
# {
#   "traceId": "tr_lq8k2m1_a7f3b9x2",
#   "actions": [...],
#   "summary": {
#     "totalDuration": 3450,
#     "totalCost": 12,
#     "parallelGroups": 2
#   }
# }

Dashboard. Click any trace_id in the actions table to see a waterfall timeline of all actions in the trace.

See also: Search & Filtering to query actions by trace ID.

Policy Engine

Define rules that are evaluated before every action. Policies can rate-limit, allowlist, blocklist, cap costs, block sensitive data, or require human approval. Set agent_name to target a specific agent, or leave it null for org-wide rules. Policies are evaluated in priority order (highest first).

Rule types

TypeConfig ExampleDescription
rate_limit{ max_actions: 100, window_seconds: 3600 }Cap actions per time window
service_allowlist{ services: ["openai", "anthropic"] }Only allow listed services
service_blocklist{ services: ["stripe"] }Block listed services
cost_limit_per_action{ max_cost_cents: 500 }Max cost per single action
payload_regex_block{ patterns: ["password", "ssn"], fields: ["input"] }Block actions with sensitive data in payload
require_approval{ services: ["stripe"], actions: ["charge"] }Require human approval before execution

Create a policy

curl -X POST https://your-instance.vercel.app/api/v1/policies \
  -H "Authorization: Bearer al_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Rate limit support-bot",
    "agent_name": "support-bot",
    "rule_type": "rate_limit",
    "config": { "max_actions": 100, "window_seconds": 3600 },
    "priority": 10,
    "enabled": true
  }'

Regex example for sensitive data blocking

curl -X POST https://your-instance.vercel.app/api/v1/policies \
  -H "Authorization: Bearer al_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Block SSN and passwords in payloads",
    "rule_type": "payload_regex_block",
    "config": {
      "patterns": ["\\b\\d{3}-\\d{2}-\\d{4}\\b", "password"],
      "fields": ["input", "output"]
    },
    "priority": 20,
    "enabled": true
  }'

Policy evaluation

Priority order: Higher priority number = evaluated first. If multiple policies match, the first blocking one wins.

Org-wide vs per-agent: Set agent_name to target a specific agent, or leave it null for org-wide rules.

Block response

When a policy blocks an action, check() and track() return:

// Pre-flight check response when blocked:
{
  "allowed": false,
  "blockReason": "Policy: Rate limit exceeded (15/10 in 3600s)"
}

// track() throws an error with this message:
// "AgentLedger: Action blocked - Policy: Rate limit exceeded (15/10 in 3600s)"

API endpoints

MethodEndpointDescription
POST/api/v1/policiesCreate a policy
GET/api/v1/policiesList all policies
PATCH/api/v1/policies/:idUpdate a policy
DELETE/api/v1/policies/:idDelete a policy

See also: Approvals for policies that require human approval before execution.

Human-in-the-Loop Approvals

When a require_approval policy matches, the action is paused and an approval request is created. A human approves or denies it from the dashboard, and the agent continues.

How it works

1. Agent calls ledger.track() and a matching policy triggers.

2. An ApprovalRequiredError is thrown with the approvalId.

3. The agent calls waitForApproval() to poll until a human decides.

4. Approvals auto-expire after 30 minutes if no action is taken.

SDK usage

approval-example.ts
import { AgentLedger, ApprovalRequiredError } from 'agentledger';

try {
  const result = await ledger.track({
    agent: 'billing-bot',
    service: 'stripe',
    action: 'charge',
    costCents: 5000,
  }, async () => {
    return await stripe.charges.create({ amount: 5000, currency: 'usd' });
  });
} catch (err) {
  if (err instanceof ApprovalRequiredError) {
    console.log('Waiting for human approval:', err.approvalId);

    // Wait up to 5 minutes for human approval
    const decision = await ledger.waitForApproval(err.approvalId, {
      timeout: 300000, // 5 minutes in ms
    });

    if (decision === 'approved') {
      // Re-execute the action now that it's approved
      await stripe.charges.create({ amount: 5000, currency: 'usd' });
    } else {
      // decision is 'denied' or 'expired' (auto-expires after 30 min)
      console.log('Action denied or expired:', decision);
    }
  } else {
    // Other errors: policy block, budget exceeded, network error, etc.
    console.error('Action failed:', err.message);
  }
}

API endpoints

MethodEndpointDescription
GET/api/v1/approvalsList pending approvals
PATCH/api/v1/approvals/:idApprove or deny (body: { "status": "approved" | "denied" })

Dashboard. The Approvals tab shows all pending requests with approve/deny buttons. Expired approvals are automatically marked as denied.

Live Streaming (SSE)

Subscribe to real-time events via Server-Sent Events. Since EventSource cannot send headers, authentication is passed as a query parameter.

Event types

EventDescription
action.newFired when a new action is logged
alert.newFired when an anomaly alert is created
heartbeatSent every 30s to keep the connection alive

Connect with filters

curl -N "https://your-instance.vercel.app/api/v1/stream?key=al_...&events=action.new&agent=my-bot&environment=production"

SDK usage

stream-example.ts
const handle = ledger.stream({
  events: ['action.new', 'alert.new'],
  agent: 'my-bot',
  onAction: (action) => {
    console.log('New action:', action.service, action.action);
  },
  onAlert: (alert) => {
    console.log('Alert:', alert.message);
  },
});

// Close the stream when done
handle.close();

Auto-reconnection. The SDK automatically reconnects with exponential backoff if the connection drops.

Anomaly Detection

AgentLedger computes statistical baselines from the last 7 days of data (updated hourly) and fires alerts when metrics deviate by more than 2 standard deviations. A minimum of 50 actions is required to establish a baseline.

Monitored metrics

MetricDescription
actions_per_hourNumber of actions per hour per agent
cost_per_actionAverage cost per action
duration_per_actionAverage duration per action
error_ratePercentage of actions with error status
service_distributionShift in which services are being called

View baselines

curl https://your-instance.vercel.app/api/v1/baselines \
  -H "Authorization: Bearer al_..."

# Response:
# {
#   "agent": "support-bot",
#   "metrics": {
#     "actions_per_hour": { "mean": 45.2, "stddev": 8.1 },
#     "cost_per_action": { "mean": 2.3, "stddev": 0.5 },
#     "error_rate": { "mean": 0.03, "stddev": 0.01 }
#   }
# }

Evaluations

Score agent actions on a 0-100 scale with optional labels and feedback. Use evaluations to track quality over time and identify regressions.

SDK (TypeScript)

evaluate.ts
await ledger.evaluate(actionId, {
  score: 85,
  label: 'correct',
  feedback: 'Response was accurate but could be more concise',
});

SDK (Python)

evaluate.py
ledger.evaluate(action_id, score=85, label="correct",
    feedback="Response was accurate but could be more concise")

API endpoints

MethodEndpointDescription
POST/api/v1/evaluationsCreate an evaluation for an action
GET/api/v1/evaluations/statsAggregated evaluation statistics

Stats response

curl https://your-instance.vercel.app/api/v1/evaluations/stats \
  -H "Authorization: Bearer al_..."

# Response:
# {
#   "avgScore": 82.4,
#   "byAgent": { "support-bot": 87.1, "billing-bot": 76.3 },
#   "byLabel": { "correct": 412, "incorrect": 38, "partial": 95 },
#   "trend": [{ "date": "2026-03-29", "avgScore": 83.1 }, ...]
# }

Rollback Hooks

Register compensating action webhooks that fire when an agent is killed or a budget is exceeded. Use rollback hooks to undo partially-completed work.

Timing. Rollback hooks fire AFTER the agent is killed or budget is exceeded, not before. They receive the completed actions from the trace as context so your compensating logic knows what to undo.

Triggers

Agent killed — the agent is permanently stopped

Budget exceeded — a budget limit is hit and actions are blocked

Register a rollback hook

curl -X POST https://your-instance.vercel.app/api/v1/rollback-hooks \
  -H "Authorization: Bearer al_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/rollback",
    "agent_name": "billing-bot",
    "triggers": ["agent.killed", "budget.exceeded"]
  }'

Webhook payload

The webhook receives the trigger reason, agent name, and the last 50 completed actions as context. Signed with HMAC-SHA256 (same as regular webhooks).

// POST to your rollback URL
// Header: X-AgentLedger-Signature: sha256=...
{
  "trigger": "agent.killed",
  "agent": "billing-bot",
  "trace_context": {
    "actions": [
      {
        "id": "act_1a2b3c",
        "service": "stripe",
        "action": "charge",
        "status": "success",
        "cost_cents": 5000,
        "created_at": "2026-03-30T11:58:00Z"
      }
    ]
  },
  "timestamp": "2026-03-30T12:00:00Z"
}

Execution history

curl https://your-instance.vercel.app/api/v1/rollback-hooks/executions \
  -H "Authorization: Bearer al_..."

Python SDK

Full-featured Python client with sync and async support.

Installation

pip install agentledger-py

Sync client

example.py
from agentledger import AgentLedger, AgentLedgerConfig, TrackOptions

ledger = AgentLedger(AgentLedgerConfig(api_key="al_..."))

# Track an action
result = ledger.track(
    TrackOptions(agent="support-bot", service="openai", action="chat_completion", cost_cents=2),
    lambda: openai.chat.completions.create(model="gpt-4", messages=messages),
)
print(result.result, result.duration_ms, result.action_id)

# Pre-flight check
check = ledger.check(TrackOptions(agent="billing-bot", service="stripe", action="charge"))
if not check.allowed:
    print(f"Blocked: {check.block_reason}")

# Log manually
ledger.log(TrackOptions(agent="data-sync", service="postgres", action="bulk_insert"),
    status="success", duration_ms=1523)

# Agent controls
ledger.pause_agent("support-bot")
ledger.resume_agent("support-bot")
ledger.kill_agent("rogue-bot")

# Evaluations
ledger.evaluate(result.action_id, score=85, label="correct",
    feedback="Accurate response")

Configuration options

config.py
from agentledger import AgentLedger, AgentLedgerConfig

ledger = AgentLedger(AgentLedgerConfig(
    api_key="al_...",
    base_url="https://your-instance.vercel.app",  # default: https://agentledger.co
    fail_open=True,         # default: True
    timeout=5.0,            # seconds, default: 5.0
    environment="staging",  # default: "production"
    on_error=lambda e: print(f"AgentLedger error: {e}"),
))

Async client

async_example.py
from agentledger import AsyncAgentLedger, AgentLedgerConfig, TrackOptions

ledger = AsyncAgentLedger(AgentLedgerConfig(api_key="al_..."))

result = await ledger.track(
    TrackOptions(agent="support-bot", service="openai", action="chat_completion"),
    lambda: openai.chat.completions.create(model="gpt-4", messages=messages),
)

LangChain integration

langchain_python.py
from agentledger import AgentLedger, AgentLedgerConfig
from agentledger.integrations.langchain import AgentLedgerCallbackHandler

ledger = AgentLedger(AgentLedgerConfig(api_key="al_..."))
handler = AgentLedgerCallbackHandler(ledger, agent="research-bot")

# Pass to any LangChain component
agent.invoke({"input": "Research AI news"}, config={"callbacks": [handler]})

OpenAI Agents integration

openai_python.py
from agentledger import AgentLedger, AgentLedgerConfig
from agentledger.integrations.openai_agents import with_agent_ledger

ledger = AgentLedger(AgentLedgerConfig(api_key="al_..."))

# Wrap the OpenAI agent runner
tracked_run = with_agent_ledger(ledger, agent="my-agent")
result = tracked_run(agent, messages)

Self-Hosting

Requirements

Node.js 18+

Supabase project (free tier works)

Vercel, Railway, Fly.io, or any Node.js host

Environment Variables

VariableDescription
NEXT_PUBLIC_SUPABASE_URLYour Supabase project URL
NEXT_PUBLIC_SUPABASE_ANON_KEYSupabase anonymous/public key
SUPABASE_SERVICE_ROLE_KEYSupabase service role key (keep secret)

Deploy to Vercel

The fastest way. Click the deploy button in the README, or:

vercel deploy --prod

Run locally

git clone https://github.com/agentledger-co/agentledger.git
cd agentledger
cp .env.local.example .env.local
# Edit .env.local with your Supabase credentials
npm install
npm run dev
# Open http://localhost:3000