Skip to main content

Ledger Agent

Keep double-entry books for everything your agent does. Post journal entries on payment events, read balances, and reconcile against signed agentic receipts.

2 min read
View MarkdownEdit on GitHub
TIME
~15 min
STACK
Node.jsno LLM required
RAIL
midazshared rail

Agents that move money need books an auditor accepts. A payment log is not accounting: it tells you a PSP call happened, not where the money sits now or whether the numbers still add up. This cookbook builds a ledger agent that posts a double-entry journal entry for every commerce event, reads balances on demand, creates accounts as new users appear, and pulls the signed agentic receipts (mandate, payment, delivery) to reconcile the books against what actually settled.

codespar_ledger is a shared rail: buy-side and sell-side agents use the same books. It records what happened; codespar_pay and codespar_charge are what move the money.

THE FLOW
Two steps, one conversation
1
Post entries
codespar_ledger
action: entry (default). Debits must equal credits, amounts in minor units
2
Read balances
codespar_ledger
action: balance. Per-asset balances for any account
3
Pull receipts
codespar_ledger
action: receipts. Signed agentic receipts for a consumer or mandate
4
Reconcile
your code
Match each receipt's payment to a journal entry; flag anything unmatched

Prerequisites

npm install @codespar/sdk
  • A csk_test_ API key from the dashboard.
  • A ledger connection. The ledger routes to the tenant's own self-hosted Lerian Midaz instance: the operator connects it once in /dashboard/auth-configs with the tenant's base_url and credentials (stored in the vault). See codespar_ledger for the connection setup.

Ledger writes are bookkeeping, not settlement. Posting an entry never moves real money, so you can run this whole cookbook in test mode without a PSP connection.

Step 1: Create the accounts

The chart of accounts is yours. A minimal setup for a marketplace agent: one wallet account per user, one revenue account, and one external account representing the PSP side.

ledger-agent.ts
import { CodeSpar } from "@codespar/sdk";

const codespar = new CodeSpar({ apiKey: process.env.CODESPAR_API_KEY! });

const session = await codespar.create("ledger-agent", {
  servers: ["midaz"],
  metadata: { source: "worker:books" },
});

await session.ledger({
  action: "account",
  alias: "@wallet/user_123",
  name: "User 123 wallet",
  type: "deposit",
});

await session.ledger({
  action: "account",
  alias: "@revenue/store",
  name: "Store revenue",
  type: "deposit",
});

await session.ledger({
  action: "account",
  alias: "@external/psp",
  name: "PSP clearing",
  type: "external",
});

Account creation is idempotent on the alias in practice: re-running setup against an existing alias fails cleanly rather than duplicating, so keep it in your worker's boot path.

Step 2: Post an entry for every payment event

Wire your payment webhook (or your agent's post-payment hook) to post a journal entry. Debits (source) must equal credits (destination), amounts are minor units (centavos, cents), and the ledger is asset-agnostic, so BRL, USD, and USDC live in the same books with a per-asset scale.

async function onPaymentSettled(event: {
  payment_id: string;
  tool_call_id: string;
  amount: number; // centavos
  consumer_id: string;
}) {
  await session.ledger({
    action: "entry",
    asset: "BRL",
    scale: 2,
    source: [{ account: `@wallet/${event.consumer_id}`, amount: event.amount }],
    destination: [{ account: "@revenue/store", amount: event.amount }],
    description: `Payment ${event.payment_id} settled`,
    metadata: {
      payment_id: event.payment_id,
      tool_call_id: event.tool_call_id,
    },
  });
}

The metadata block is the join key for Step 4: stamp every entry with the payment_id and tool_call_id that caused it, and reconciliation becomes a lookup instead of an investigation.

Entries can be n:n. A marketplace sale that splits into seller payout plus platform fee is one balanced entry with one source and two destination legs, not two entries that can drift apart.

Step 3: Read balances

const balance = await session.ledger({
  action: "balance",
  account: "<account-uuid>",
});
// balance lists per-asset available amounts for the account

Use this for pre-flight checks (is there enough in the wallet before the agent commits to a purchase?) and for the end-of-day snapshot your reconciliation report starts from.

Step 4: Pull receipts and reconcile

What a receipt looks like when it comes back:

Agentic receipt
rcp_8f2a41c9
signed · Ed25519
Mandate — the permission
mandatecm_gWEZO68q
capBRL 500.00 / month
spent after thisBRL 179.90
Payment — the settlement
railpix
amountBRL 179.90
end-to-end refE18236120…7401
Delivery — the proof
evidencestore order #1049-B confirmed
chain a91c…04be 7d2f…c918verifiable offline via did:web
A signed agentic receipt binding the mandate, the settled payment, and the delivery proof, chained into the audit ledger and verifiable offline.

Every governed payment produces a signed agentic receipt (the Control Record): the chain from mandate to payment to delivery for that transaction. action: "receipts" lists them for a consumer or mandate; action: "receipt" fetches one.

const receipts = await session.ledger({
  action: "receipts",
  consumer_id: "con_abc123",
  mandate_id: "cm_gW...", // optional: narrow to one mandate
});

for (const r of receipts.items) {
  const entry = await books.findEntryByPaymentId(r.payment_id); // your Step 2 metadata
  if (!entry) {
    await flagForReview({
      reason: "receipt_without_entry",
      receipt_id: r.receipt_id,
      payment_id: r.payment_id,
    });
  }
}

Run the match in both directions: a receipt without an entry means the books missed a settlement; an entry without a receipt means the books claim money moved that the platform never attested. Both are findings an auditor cares about, and both come out of one loop over receipts.items.

MCP variant: books in natural language

The same rail is exposed through the CodeSpar MCP server, so an operator can work the books from Claude, Codex, or any MCP host without writing the worker above:

"Post a journal entry: R$150,00 from @wallet/user_123 to @revenue/store,
description 'Order #1234 settled'. Then show me the revenue balance and
pull the receipts for mandate cm_gW... so we can check they match."

The agent calls codespar_ledger three times (entry, balance, receipts) and reports the mismatches. Governance is identical: every call lands on the audit chain either way.

What a receipt proves today

Be precise about what you are reconciling against. A receipt today reliably binds the mandate (what the agent was authorized to spend, signed) and the payment (what was executed, with rail-level identifiers). The delivery leg is evolving: binding an external proof of delivery, such as a fiscal document or carrier confirmation, into the signed record is not complete for every rail. Treat the receipt as proof of authorized payment, and keep delivery evidence in your own records until the binding lands. The receipt read itself is an audited event, so pulling receipts leaves its own trace on the chain.

Next steps

Ledger Agent | CodeSpar