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
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 > RunDeploy 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 devInstall the SDK and start tracking
npm install agentledgerimport { 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 agentledgerThe SDK has zero external dependencies and works in Node.js 18+. It also works in Bun and Deno.
Configuration
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
});| Option | Default | Description |
|---|---|---|
apiKey | required | Your API key (starts with al_) |
baseUrl | https://agentledger.co | Your AgentLedger API endpoint |
failOpen | true | If true, actions proceed when AgentLedger is unreachable |
timeout | 5000 | API call timeout in milliseconds |
onError | undefined | Callback 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.
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);
}| Option | Type | Description |
|---|---|---|
agent | string | Name of the agent performing the action (required) |
service | string | Service being called, e.g. "openai", "stripe" (required) |
action | string | Action being performed, e.g. "send_email" (required) |
costCents | number | Estimated cost in cents |
metadata | Record<string, unknown> | Custom key-value pairs logged with the action |
traceId | string | Trace ID to group related actions (see Traces) |
input | any | Input data (e.g. prompt, request body) stored for debugging |
output | any | Explicit output value to log. Overrides captureOutput |
captureOutput | boolean | Auto-capture fn return value as output (default: false) |
ledger.check(options)
Pre-flight check without executing the action. Useful before expensive operations.
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.
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 agentLangChain Integration
Drop-in callback handler that auto-tracks tool calls, LLM completions, and chain runs.
npm install agentledger langchain @langchain/coreimport { 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.
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.
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
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.
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/actions | Log an agent action |
GET | /api/v1/actions | List actions (paginated) |
POST | /api/v1/check | Pre-flight budget/status check |
GET | /api/v1/stats | Dashboard summary stats |
GET | /api/v1/agents/:name | Agent details + recent actions |
POST | /api/v1/agents/:name/pause | Pause an agent |
POST | /api/v1/agents/:name/resume | Resume a paused agent |
POST | /api/v1/agents/:name/kill | Permanently kill an agent |
POST | /api/v1/budgets | Create or update a budget |
GET | /api/v1/alerts | List 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
| Event | Fired When |
|---|---|
action.logged | Any agent action is recorded |
agent.paused | An agent is paused |
agent.killed | An agent is permanently killed |
agent.resumed | A paused agent is resumed |
budget.exceeded | A budget limit is reached |
budget.warning | A budget crosses 75% usage |
alert.created | Any anomaly alert is created |
batch.logged | A 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
// 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
| Channel | Config Required | Use Case |
|---|---|---|
email | Email address | General notifications |
slack | Webhook URL | Team chat alerts |
discord | Webhook URL | Team chat alerts |
pagerduty | Routing key | On-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 → infoList active channels
curl https://your-instance.vercel.app/api/v1/notifications \
-H "Authorization: Bearer al_..."
// Returns all configured channels with their events and statusAPI 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:
| Period | Resets At |
|---|---|
daily | Midnight UTC |
weekly | Monday midnight UTC |
monthly | 1st 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)
const ledger = new AgentLedger({
apiKey: 'al_...',
environment: 'staging', // 'production' | 'staging' | 'development' | any string
});SDK (Python)
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.
Search & Filtering
Query the action log with powerful filters. All parameters are optional and can be combined.
| Parameter | Type | Description |
|---|---|---|
agent | string | Filter by agent name |
service | string | Filter by service (e.g. openai, stripe) |
status | string | Filter by status: success, error, blocked |
from | ISO 8601 | Start of time range |
to | ISO 8601 | End of time range |
trace_id | string | Filter by trace ID |
search | string | Full-text search across action metadata |
cursor | string | Cursor for pagination (from previous response) |
Example: filtered query
curl "https://your-instance.vercel.app/api/v1/actions?agent=support-bot&status=error&from=2026-03-01T00:00:00Z&to=2026-03-30T00:00:00Z" \
-H "Authorization: Bearer al_..."
# Paginate through large result sets with cursor
curl "https://your-instance.vercel.app/api/v1/actions?agent=support-bot&cursor=eyJpZCI6MTIzfQ" \
-H "Authorization: Bearer al_..."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).
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
| Type | Config Example | Description |
|---|---|---|
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
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/policies | Create a policy |
GET | /api/v1/policies | List all policies |
PATCH | /api/v1/policies/:id | Update a policy |
DELETE | /api/v1/policies/:id | Delete 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
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
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/approvals | List pending approvals |
PATCH | /api/v1/approvals/:id | Approve 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
| Event | Description |
|---|---|
action.new | Fired when a new action is logged |
alert.new | Fired when an anomaly alert is created |
heartbeat | Sent 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
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
| Metric | Description |
|---|---|
actions_per_hour | Number of actions per hour per agent |
cost_per_action | Average cost per action |
duration_per_action | Average duration per action |
error_rate | Percentage of actions with error status |
service_distribution | Shift 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)
await ledger.evaluate(actionId, {
score: 85,
label: 'correct',
feedback: 'Response was accurate but could be more concise',
});SDK (Python)
ledger.evaluate(action_id, score=85, label="correct",
feedback="Response was accurate but could be more concise")API endpoints
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/evaluations | Create an evaluation for an action |
GET | /api/v1/evaluations/stats | Aggregated 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-pySync client
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
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
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
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
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
| Variable | Description |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | Your Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase anonymous/public key |
SUPABASE_SERVICE_ROLE_KEY | Supabase service role key (keep secret) |
Deploy to Vercel
The fastest way. Click the deploy button in the README, or:
vercel deploy --prodRun 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