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 "text-violet-400">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/miken1988/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
"text-violet-400">import { AgentLedger } "text-violet-400">from 'agentledger';

"text-violet-400">const ledger = "text-violet-400">new AgentLedger({
  apiKey: process.env.AGENTLEDGER_KEY,
});

// Wrap any agent action
"text-violet-400">const result = "text-violet-400">await ledger.track({
  agent: 'support-bot',
  service: 'slack',
  action: 'send_message',
}, "text-violet-400">async () => {
  "text-violet-400">return "text-violet-400">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
"text-violet-400">const ledger = "text-violet-400">new AgentLedger({
  apiKey: 'al_...',              // Required. Get "text-violet-400">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
"text-violet-400">const { result, allowed, durationMs, actionId } = "text-violet-400">await ledger.track({
  agent: 'support-bot',       // Agent name
  service: 'sendgrid',        // Service being called
  action: 'send_email',       // Action being per"text-violet-400">formed
  costCents: 1,               // Optional: estimated cost
  metadata: { to: 'user@' },  // Optional: custom metadata
}, "text-violet-400">async () => {
  "text-violet-400">return "text-violet-400">await sendEmail(to, subject, body);
});

ledger.check(options)

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

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

"text-violet-400">if (!allowed) {
  console.log('Blocked:', blockReason);
}

ledger.log(options)

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

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

Agent Controls

"text-violet-400">await ledger.pauseAgent('support-bot');  // Blocks all future actions
"text-violet-400">await ledger.resumeAgent('support-bot'); // Resumes the agent
"text-violet-400">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
"text-violet-400">import { AgentLedger } "text-violet-400">from 'agentledger';
"text-violet-400">import { AgentLedgerCallbackHandler } "text-violet-400">from 'agentledger/integrations/langchain';

"text-violet-400">const ledger = "text-violet-400">new AgentLedger({ apiKey: 'al_...' });

"text-violet-400">const handler = "text-violet-400">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
"text-violet-400">const agent = createReactAgent({
  llm: "text-violet-400">new ChatOpenAI({ callbacks: [handler] }),
  tools,
});

"text-violet-400">await agent.invoke(
  { input: 'Research the latest AI "text-violet-400">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
"text-violet-400">import { AgentLedger } "text-violet-400">from 'agentledger';
"text-violet-400">import { createToolExecutor } "text-violet-400">from 'agentledger/integrations/openai';

"text-violet-400">const ledger = "text-violet-400">new AgentLedger({ apiKey: 'al_...' });

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

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

// Create the executor
"text-violet-400">const execute = createToolExecutor(ledger, 'my-agent', handlers, serviceMap);

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

Wrap individual functions

"text-violet-400">import { withAgentLedger } "text-violet-400">from 'agentledger/integrations/openai';

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

// Use exactly like the original
"text-violet-400">await trackedSendEmail(to, subject, body);

MCP Server Integration

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

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

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

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

server.tool('search_web', { query: z.string() }, "text-violet-400">async (args) => {
  "text-violet-400">return "text-violet-400">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

"text-violet-400">import { wrapMCPTool } "text-violet-400">from 'agentledger/integrations/mcp';

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

Express / Generic Integration

Express middleware

express-example.ts
"text-violet-400">import { agentLedgerMiddleware } "text-violet-400">from 'agentledger/integrations/express';

// Track spec"text-violet-400">ific routes
app.post('/api/send-email', agentLedgerMiddleware(ledger, {
  agent: 'email-bot',
  service: 'sendgrid',
  action: 'send_email',
}), emailHandler);

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

Wrap any function

Works with any framework — no Express required.

"text-violet-400">import { trackFunction } "text-violet-400">from 'agentledger/integrations/express';

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

// Same signature as the original "text-violet-400">function
"text-violet-400">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" }
  }'

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

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": [...] }

Verify signatures

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

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

  "text-violet-400">if (signature !== expected) {
    "text-violet-400">return res.status(401).send('Invalid signature');
  }

  // Process the event
  "text-violet-400">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.

API Key Management

Create up to 5 active API keys per organization. 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.

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/miken1988/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