@empyre/ledger-sdk
v1.1.0
Published
Ledger — the financial operating system for autonomous companies: double-entry accounting, revenue ingestion, runway, an autonomous CFO, deterministic agent spending controls, and a `ledger` CLI.
Downloads
265
Maintainers
Readme
@empyre/ledger-sdk
The financial operating system for autonomous companies — Ledger by Empyre.
Send your product's revenue into real double-entry books, read cash and runway, ask a CFO what changed, and give your AI agents spending limits they cannot exceed.
Zero runtime dependencies. Node 18+, Deno, Bun, browsers, edge runtimes.
npm install @empyre/ledger-sdkTwo things to know first
1. Every amount is an integer number of minor units. 1999 is $19.99. There is no float anywhere in this SDK, and there should be none in your code either — 0.1 + 0.2 !== 0.3, and a financial system adds numbers millions of times. Use toMinor() where you touch human input and integers everywhere else.
2. A test key writes data that never reaches the books. ldg_test_… events are stored and visible, but produce no journal entry. That isn't a courtesy: developing an integration against production revenue is how a founder's P&L fills with fake sales during a sprint.
Quick start
import { Ledger, toMinor } from "@empyre/ledger-sdk";
const ledger = new Ledger({ apiKey: process.env.LEDGER_API_KEY });
// Record a sale. With an idempotency key, retrying is free — the same order
// can never be counted twice.
await ledger.commerce.record({
kind: "sale",
amountMinor: toMinor("49.00"),
currency: "USD",
customerRef: "cus_42",
idempotencyKey: order.id,
});Get a key at ledger.empyre.dev → Developers. It's shown once; Ledger stores only a hash and cannot show it again.
Money
The single most common way to get finance code wrong is to multiply by 100.
import { toMinor, fromMinor } from "@empyre/ledger-sdk";
toMinor("19.99") // 1999
toMinor(19.99) // 1999 — not 1998, which `19.99 * 100` gives you
toMinor("1000", "JPY") // 1000 — yen has no minor unit
toMinor("1.234", "KWD") // 1234 — dinar has three
toMinor("2.345") // 235 — half-up, as invoicing and tax specify
toMinor("-2.345") // -235 — away from zero, both directions
fromMinor(1999) // "19.99"
fromMinor(1000, "JPY") // "1000"toMinor shifts the decimal by string manipulation, never by multiplication — multiplying is precisely the step that reintroduces the error it exists to prevent.
Reading the books
const dash = await ledger.dashboard();
dash.headline.cash_minor // 4_827_00
dash.headline.runway_months // 7.2, or null when profitable
dash.headline.confidence // "low" | "medium" | "high"Always read confidence. A projection built on six weeks of volatile history is a different object from one built on two years, and Ledger tells you which you have rather than presenting both identically.
const pnl = await ledger.profitAndLoss(entityId, { detailed: true });
pnl.gross_margin_bps // 6200 = 62.0%, or null if there's no revenueA margin of null means no revenue to divide by — not zero. A pre-revenue company doesn't have a 0% margin; charting it as one misrepresents it as catastrophically unprofitable.
Check the books actually close
const tb = await ledger.trialBalance(entityId);
if (!tb.balanced) {
// Every figure derived from these books is suspect. Ledger returns the
// verdict rather than leaving you to compare the totals yourself.
}Runway and what to do about it
const runway = await ledger.runway();
runway.level // "healthy" | "watch" | "at_risk" | "critical"
runway.cash_out_date // "2026-11-04", or null when profitable
const change = await ledger.requiredChange({ targetRunwayMonths: 6 });
console.log(change.summary);
// "To maintain 6 months of runway without external financing, monthly
// contribution profit must increase by approximately 31.2% within the next
// 60 days, or monthly cash expenses must decline by approximately $41,700."options returns several independent levers — cut costs, grow contribution, raise price, raise capital — each with a certainty, because they are not interchangeable. method states the arithmetic so you can check it.
Requires the Pro plan.
The CFO
const answer = await ledger.askCfo("Which customers are losing me money?");
console.log(answer.answer);
console.log(answer.sources); // [{ name: "get_customer_profitability", ok: true }]Every figure in the answer is computed by Ledger from the books. The model decides which questions to ask and how to explain them — it never decides what the numbers are. sources lists the tools that produced them, which is what makes the answer checkable rather than merely fluent.
Agent spending
const result = await ledger.requestSpend({
amountMinor: toMinor("240.00"),
purpose: "Renew monitoring subscription",
vendorName: "Datadog",
requesterKind: "agent",
agentRole: "cto",
});
result.decision // "auto_approved" | "approved" | "needs_approval" | "denied"
result.reason // why, in words
result.checks // every limit evaluated, and where this request stoodThe decision is arithmetic over your policies, not a model's judgement. An agent can request money; it can never decide it may have it. An agent with no policy is denied outright — that is the point of the feature, not an edge case.
Webhooks
import { verifyWebhook } from "@empyre/ledger-sdk";
app.post("/webhooks/ledger", async (req, res) => {
const ok = await verifyWebhook({
payload: req.rawBody, // the RAW bytes, not a re-serialised object
header: req.headers["ledger-signature"],
secret: process.env.LEDGER_WEBHOOK_SECRET,
});
if (!ok) return res.status(400).end();
// …
});Two things this handles that a naive check misses:
- The timestamp is inside the signed material. Signing the body alone lets anyone who captured one delivery replay it forever. A delivery older than the tolerance (default 5 minutes) is refused even though its signature is genuine.
- Comparison is constant-time. A fast
===leaks, byte by byte, how much of a forged signature was correct.
Pass the raw body. Re-serialising a parsed object changes the bytes and the signature will not match.
Errors
import { LedgerError, LedgerPaywallError } from "@empyre/ledger-sdk";
try {
await ledger.runway();
} catch (error) {
if (error instanceof LedgerPaywallError) {
// Not a failure — an unbought capability.
console.log(`Available on ${error.requiredPlanName}`);
} else if (error instanceof LedgerError && error.retryable) {
// Timeout, 429 or 5xx. The SDK already retried twice with backoff.
}
}A 4xx is never retried — the request was wrong, and sending it again will be wrong the same way. Retryable failures use exponential backoff with jitter, so a fleet of agents recovering from an incident doesn't arrive in lockstep and knock the service over again.
Configuration
| Option | Default | |
|---|---|---|
| apiKey | LEDGER_API_KEY | required |
| baseUrl | LEDGER_BASE_URL or https://api.empyre.dev | |
| timeoutMs | 30000 | per request |
| maxRetries | 2 | retryable failures only |
| fetch | global fetch | override for tests |
Links
- Ledger — ledger.empyre.dev
- Empyre — empyre.dev · AI agents that build and run your business
- Relay — relay.empyre.dev · OAuth for AI agents
- Vault — vault.empyre.dev · secrets and signing for AI agents
MIT © EmpyreDev, Inc.
