Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: CI

on:
pull_request:
branches:
- main
push:
branches:
- main

permissions:
contents: read

jobs:
validate:
name: validate
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
cache: npm

- name: Install dependencies
run: npm ci

- name: Typecheck
run: npm run typecheck

- name: Test
run: npm test

- name: Build
run: npm run build

- name: Audit production dependencies
run: npm audit --omit=dev
53 changes: 30 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
# AgentLedger

**The financial control plane for autonomous agents.**
<p align="center">
<img src="docs/assets/agentledger.png" alt="AgentLedger" width="180" />
</p>

<p align="center"><strong>The financial control plane for autonomous agents.</strong></p>

<p align="center">Financial state and policy in; machine-readable recommendations and capital decisions out.</p>

<p align="center">
<a href="https://github.com/chin0312/agentledger/actions/workflows/ci.yml"><img src="https://github.com/chin0312/agentledger/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI status" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT license" /></a>
</p>

> Financial execution requires financial governance.

AgentLedger converts financial state and policy into machine-readable recommendations and capital decisions for autonomous agents. It is an API-only, deterministic, explainable service that sits between agent intent and capital movement.
Autonomous agents can already spend, trade and purchase services. AgentLedger provides the deterministic, explainable financial decision layer between agent intent and capital movement.

```text
Calling agent
↓
financial state + proposed action + policy
↓
AgentLedger
↓
normalize → analyze → evaluate policy
↓
recommendation / APPROVE / CAUTION / REJECT
```
| Service | Question | Price |
| --- | --- | --- |
| Financial Health | How am I? | Free |
| Recommendations | What should I do next? | 0.01 USDT/call |
| Policy Guard | Should this capital move? | 0.02 USDT/call |

Autonomous agents can already spend, trade and purchase services. AgentLedger supplies the financial decision layer that helps them understand operating health and govern spending before money moves.
Quick links: [Production API](https://agentledger-one.vercel.app) · [OpenAPI](https://agentledger-one.vercel.app/openapi.json) · [GitHub](https://github.com/chin0312/agentledger) · [OKX.AI ASP #11336](https://www.okx.ai/agents/11336)

AgentLedger is not a wallet, accounting system, tax system, investment adviser, portfolio manager, trading bot or execution engine. It never signs or executes capital movement.

Expand All @@ -42,15 +47,6 @@ AgentLedger is not a wallet, accounting system, tax system, investment adviser,

`POST /api/evaluate-action` evaluates `service_purchase` and `capital_allocation` actions against caller-supplied financial state and policy. It returns an explainable `approve`, `caution` or `reject` decision. The hosted route uses x402 on X Layer.

Compatibility primitives remain available and free:

```text
POST /api/can-i-spend
POST /api/can-i-allocate
```

They are not initial Marketplace services.

## Agent-native architecture

AgentLedger does not need control of an agent's wallet. A calling agent can use Agentic Wallet, exchange APIs, wallet tools, accounting tools, execution systems or internal strategy state to gather factual financial state and send it to AgentLedger.
Expand Down Expand Up @@ -107,6 +103,17 @@ curl -X POST http://localhost:3000/api/demo/company-health \

The seeded company has marketplace, research-service and client inflows, repeated payments to agent vendors, stablecoin activity across USDT/USDC/USDG, spend acceleration and one failed transaction.

## Compatibility API

The original specialist primitives remain available and free for compatibility:

```text
POST /api/can-i-spend
POST /api/can-i-allocate
```

They are not initial Marketplace services; use Policy Guard for the Marketplace-facing generalized control flow.

## API examples

### Financial Health from caller-provided state
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Reporting a vulnerability

Please report suspected vulnerabilities privately to the repository maintainers before opening a public issue. Include the affected endpoint or file, reproduction steps, impact and a suggested mitigation when available. Do not include credentials, private keys or other sensitive material in the report.
Please use GitHub's private [Report a vulnerability](https://github.com/chin0312/agentledger/security/advisories/new) flow from the repository Security tab before opening a public issue. Include the affected endpoint or file, reproduction steps, impact and a suggested mitigation when available. Do not include credentials, private keys or other sensitive material in the report. If the private reporting form is unavailable, open a minimal public issue asking maintainers for a secure contact path without including exploit details.

## Credential handling

Expand Down
12 changes: 6 additions & 6 deletions app/openapi.json/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,18 +245,18 @@ export function GET(request: Request): Promise<Response> {
monthlySpendLimit: { type: "number", minimum: 0 }, reservePct: { type: "number", minimum: 0, maximum: 100, default: 20 }, cautionBudgetUtilizationPct: { type: "number", minimum: 0, maximum: 100, default: 75 }, criticalBudgetUtilizationPct: { type: "number", minimum: 0, maximum: 100, default: 90 }, maxProviderConcentrationPct: { type: "number", minimum: 0, maximum: 100, default: 35 }, maxSingleActionAmount: { type: "number", minimum: 0 }, maxStrategyAllocationPct: { type: "number", minimum: 0, maximum: 100, default: 35 }, maxStrategyAllocationAbsolute: { type: "number", minimum: 0 }, dailyLossLimit: { type: "number", minimum: 0 }, cautionLossUtilizationPct: { type: "number", minimum: 0, maximum: 100, default: 70 }, criticalLossUtilizationPct: { type: "number", minimum: 0, maximum: 100, default: 90 },
},
},
FinancialProvider: { type: "object", required: ["provider", "spend", "transactions"], additionalProperties: false, properties: { provider: { type: "string", minLength: 1, maxLength: 256 }, spend: { type: "number", minimum: 0 }, transactions: { type: "integer", minimum: 0 }, failedTransactions: { type: "integer", minimum: 0 } } },
FinancialProvider: { type: "object", required: ["provider", "spend", "transactions"], additionalProperties: false, description: "Provider-level successful outgoing activity. failedTransactions counts failed outgoing attempts separately and may be greater than transactions.", properties: { provider: { type: "string", minLength: 1, maxLength: 256 }, spend: { type: "number", minimum: 0 }, transactions: { type: "integer", minimum: 0, description: "Successful outgoing payments to this provider." }, failedTransactions: { type: "integer", minimum: 0, description: "Failed outgoing attempts to this provider; not included in transactions or spend." } } },
ProvidedFinancialState: {
type: "object",
required: ["periodDays", "cashFlow"],
additionalProperties: false,
description: "Caller-supplied factual financial state. AgentLedger derives conclusions and does not independently attest to these values.",
description: "Caller-supplied factual financial state. AgentLedger derives conclusions and does not independently attest to these values. Provider lists may be partial. Provider spend cannot exceed total outflows by more than $0.01; successful provider transaction counts cannot exceed explicit successful outgoingTransactions; and explicit failedTransactions must include all provider failedTransactions.",
properties: {
periodDays: { type: "integer", minimum: 1, maximum: 90 },
cashFlow: { type: "object", required: ["inflows", "outflows"], additionalProperties: false, properties: { inflows: { type: "number", minimum: 0 }, outflows: { type: "number", minimum: 0 }, incomingTransactions: { type: "integer", minimum: 0 }, outgoingTransactions: { type: "integer", minimum: 0 } } },
spendVelocity: { type: "object", additionalProperties: false, properties: { firstHalf: { type: "number", minimum: 0 }, secondHalf: { type: "number", minimum: 0 } } },
providers: { type: "array", maxItems: 100, items: { $ref: "#/components/schemas/FinancialProvider" } },
failedTransactions: { type: "integer", minimum: 0 },
cashFlow: { type: "object", required: ["inflows", "outflows"], additionalProperties: false, description: "Inflows and outflows for the period. Transaction counts describe successful transactions; failed attempts are tracked separately.", properties: { inflows: { type: "number", minimum: 0 }, outflows: { type: "number", minimum: 0 }, incomingTransactions: { type: "integer", minimum: 0, description: "Successful incoming transactions." }, outgoingTransactions: { type: "integer", minimum: 0, description: "Successful outgoing transactions. Provider transaction totals cannot exceed this value when supplied." } } },
spendVelocity: { type: "object", additionalProperties: false, description: "Observed spend in the two half-periods. The values are used for velocity and are not required to equal total outflows when the caller reports a partial observation.", properties: { firstHalf: { type: "number", minimum: 0 }, secondHalf: { type: "number", minimum: 0 } } },
providers: { type: "array", maxItems: 100, description: "Successful outgoing provider payments. Entries with the same provider name are aggregated case-insensitively; the list may be a subset of total spending.", items: { $ref: "#/components/schemas/FinancialProvider" } },
failedTransactions: { type: "integer", minimum: 0, description: "Total failed transactions. If omitted, known provider-level failures are derived; if supplied, it must be at least their sum." },
},
},
ProvidedCompanyHealthRequest: { type: "object", required: ["source", "financialState"], properties: { source: { type: "string", enum: ["provided_state"] }, financialState: { $ref: "#/components/schemas/ProvidedFinancialState" }, address: { type: "string", description: "Optional caller label; it is not verified by AgentLedger." }, monthlyBudget: { type: "number", minimum: 0 }, policy: { $ref: "#/components/schemas/FinancialPolicy" } } },
Expand Down
Binary file added docs/assets/agentledger.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 26 additions & 2 deletions lib/financial-context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { NormalizedTransaction, ProvidedFinancialState } from "./types";

export const FINANCIAL_STATE_TOLERANCE = 0.01;
export const MAX_AGGREGATE_TRANSACTION_COUNT = 1_000_000;

export type NormalizedProviderState = {
provider: string;
spend: number;
Expand Down Expand Up @@ -83,12 +86,33 @@ export function normalizeFinancialState(state: ProvidedFinancialState): Normaliz
}
}

const failedTransactions = state.failedTransactions ?? 0;
const providerSpendTotal = [...providers.values()].reduce((sum, provider) => sum + provider.spend, 0);
if (providerSpendTotal > state.cashFlow.outflows + FINANCIAL_STATE_TOLERANCE) {
throw new Error("financialState.providers.spend total must not exceed cashFlow.outflows");
}

const providerSuccessfulTransactionTotal = [...providers.values()].reduce((sum, provider) => sum + provider.transactions, 0);
if (state.cashFlow.outgoingTransactions !== undefined && providerSuccessfulTransactionTotal > state.cashFlow.outgoingTransactions) {
throw new Error("financialState.providers.transactions total must not exceed cashFlow.outgoingTransactions");
}
if (providerSuccessfulTransactionTotal > MAX_AGGREGATE_TRANSACTION_COUNT) {
throw new Error("financialState provider transaction totals are unreasonably large");
}

const providerFailedTotal = [...providers.values()].reduce((sum, provider) => sum + provider.failedTransactions, 0);
if (providerFailedTotal > MAX_AGGREGATE_TRANSACTION_COUNT) {
throw new Error("financialState provider failed transaction totals are unreasonably large");
}
if (state.failedTransactions !== undefined && state.failedTransactions < providerFailedTotal) {
throw new Error("financialState.failedTransactions must include all provider failedTransactions");
}

const failedTransactions = state.failedTransactions ?? providerFailedTotal;
assertCount(failedTransactions, "failedTransactions");
const incomingTransactions = state.cashFlow.incomingTransactions ?? (state.cashFlow.inflows > 0 ? 1 : 0);
const outgoingTransactions = state.cashFlow.outgoingTransactions ?? (
providers.size > 0
? [...providers.values()].reduce((sum, provider) => sum + provider.transactions, 0)
? providerSuccessfulTransactionTotal
: state.cashFlow.outflows > 0
? 1
: 0
Expand Down
Loading
Loading