--- name: journalize-ledger description: Use this skill whenever interacting with the Journalize API or MCP server (journalize.dev) — submitting accounting entries, checking balances, or generating financial reports for an AI agent's own bookkeeping. Covers double-entry bookkeeping from first principles (no prior accounting knowledge assumed), Beancount directive syntax, the exact request-signing envelope, and the full paid/free endpoint and tool list with pricing. Trigger this whenever a task involves recording a transaction, tracking spend, checking a balance, or producing a balance sheet / income statement via Journalize. --- # Journalize — agent-native double-entry ledger Journalize is an accounting backend built for AI agents to be the primary client, not humans. Every write and most reads are paid per-call via x402 (USDC on Base); a signed request replaces the login session you'd use on a human-facing API. This document assumes **zero prior accounting knowledge**. If you already know double-entry bookkeeping, skip to "Calling the API." ## Part 1 — Double-entry bookkeeping, from scratch ### The one idea everything else follows from Every financial event touches **at least two accounts**, and the amounts involved always **sum to zero** across those accounts. Money never appears or disappears — it moves from one place to another, and the record captures both sides of that move. Concretely: if you pay $4.50 for coffee from your checking account, two things happened, not one — your checking balance went down by $4.50, *and* you now have $4.50 worth of coffee-expense. One entry, two postings, they net to zero: ``` 2026-03-15 * "Coffee" Expenses:Coffee 4.50 USD Assets:Checking -4.50 USD ``` That's a complete, valid transaction. Nothing else is required. The two amounts (`4.50` and `-4.50`) sum to zero — if they didn't, the system rejects it outright (see "What gets rejected," below). ### The five account categories Every account name is a colon-separated path, and the **first segment must be one of exactly five words**: | Root | What lives here | Typical direction | |---|---|---| | `Assets` | Things you own or hold — a checking account, a wallet balance, inventory | balance usually increases with money coming in | | `Liabilities` | Things you owe — a credit card balance, a loan | balance usually increases with money you'll have to pay back | | `Equity` | Net worth / starting balances / owner's stake | mostly used once, to record where a balance came from | | `Income` | Money you earned | balance goes *negative* when you earn (this feels backwards at first — see below) | | `Expenses` | Money you spent | balance goes *positive* when you spend | Everything else after the first colon is just a label you choose — `Assets:Checking`, `Assets:Checking:USD`, `Expenses:API-Costs:Anthropic` are all valid, arbitrarily deep. There's no fixed list beyond the five roots; you invent the hierarchy that makes sense for what you're tracking. **Why Income goes negative**: the whole system is zero-sum. If `Expenses:Coffee` goes up by 4.50 when you spend, and the transaction has to net to zero, whatever's on the other side has to go down by 4.50. Applied to earning money: if you get paid $100, `Assets:Checking` goes up 100, so `Income:Consulting` has to go *down* 100 (i.e., more negative) to balance. This is standard across every accounting system, not a Journalize quirk — get used to reading a negative Income balance as "you earned this much," not as a debt. ### Before you can post to an account, it has to be opened Every account needs a one-time `open` directive before any transaction can reference it: ``` 2026-01-01 open Assets:Checking USD 2026-01-01 open Expenses:Coffee USD ``` The `USD` is an optional currency constraint (you can list several, comma-separated, or omit it to allow any currency). Posting to an account that was never opened is rejected — this is the single most common first-time error, and the fix is always "open the account first." ### What a transaction directive actually looks like ``` DATE FLAG "NARRATION" ACCOUNT-1 AMOUNT CURRENCY ACCOUNT-2 AMOUNT CURRENCY [more postings if needed] ``` - **DATE**: `YYYY-MM-DD`. - **FLAG**: `*` for a confirmed/cleared transaction (the normal case). `!` exists for "needs review" but you'll rarely need it. - **NARRATION**: a quoted string describing what happened. Free text. - **Postings**: two or more `ACCOUNT AMOUNT CURRENCY` lines, indented. Amounts across all postings in one transaction must sum to exactly zero. You can have more than two postings (e.g. splitting one payment across two expense categories) as long as the total still nets to zero. A three-posting example — one payment, split across two expense categories: ``` 2026-03-20 * "Team lunch, partial reimbursement" Expenses:Meals 30.00 USD Assets:Checking -30.00 USD ``` vs. a split: ``` 2026-03-20 * "Mixed purchase" Expenses:Meals 18.00 USD Expenses:Office-Supplies 12.00 USD Assets:Checking -30.00 USD ``` ### Starting balances — the one case where you use `Equity` The first time you record an account's existing balance (not a new transaction, just "this account already has $500 in it"), the other side of that entry goes to `Equity:Opening-Balances` — a bookkeeping convention for "money that existed before you started tracking it," which isn't income or an expense, it's just your starting point: ``` 2026-01-01 open Assets:Checking USD 2026-01-01 open Equity:Opening-Balances USD 2026-01-02 * "Opening balance" Assets:Checking 500.00 USD Equity:Opening-Balances -500.00 USD ``` ### What gets rejected, and why Journalize validates every submission against your *entire* prior history before accepting it (not just the new entry in isolation) — this is real Beancount, not a simplified imitation: - **Unbalanced transaction** — postings don't sum to zero. Rejected, nothing saved. - **Posting to an unopened account** — you referenced an account with no `open` directive anywhere in your history. Rejected, nothing saved. - **Malformed syntax** — bad date, missing quote, wrong indentation. Rejected before anything else is even attempted. A rejected write costs nothing (see Pricing below) — nothing is charged unless the write actually succeeds. ## Part 2 — Calling the API ### Two separate keys, two separate purposes Journalize uses **two independent signing domains** — don't confuse them: 1. **Ledger identity** (Ed25519) — proves who you are to Journalize itself. You get this by calling `register` once. We generate it, hand you the private key exactly one time, and never store it — losing it means losing access, there's no recovery. 2. **Payment authorization** (your own EVM/x402 wallet) — a completely different keypair you already control, used only to authorize USDC payments. Journalize never sees or generates this one. ### Step 1 — Register ``` POST https://journalize.dev/register ``` or the `register` MCP tool, no arguments. Free, no signature required — there's nothing to sign with yet. Returns: ```json {"public_key": "...", "private_key": "..."} ``` Store both. The private key is shown exactly once. ### Step 2 — Sign every subsequent call Every call after registration must be signed with your Ed25519 private key. The exact bytes you sign differ slightly by transport: **REST**: sign `METHOD\nPATH\nSORTED_QUERY_STRING\nSHA256(BODY)\nNONCE\nTIMESTAMP` (joined with newlines), and send the result as four headers: `X-Public-Key`, `X-Nonce`, `X-Timestamp`, `X-Signature`. `NONCE` is any value you've never used before (a UUID is fine); `TIMESTAMP` is an ISO 8601 UTC timestamp within about 5 minutes of now. **MCP**: sign `TOOL_NAME\nJSON(ARGUMENTS, sorted keys, compact)\nNONCE\nTIMESTAMP`, and pass `public_key`, `nonce`, `timestamp`, `signature` as ordinary tool arguments alongside the tool's own arguments. Both: a nonce can only be used once, ever, per key. Sign a fresh nonce for every call, including retries. ### Step 3 — For paid calls, handle the 402 / payment-required round trip Call any paid endpoint/tool without payment first, and you'll get back price and payment instructions instead of the real result (an HTTP `402` on REST; a normal `{"status": "payment_required", ...}` result on MCP — not an error, just information). Construct and attach an x402 payment for the amount shown, then retry the *same call* with a fresh nonce and the payment attached (`X-PAYMENT` header on REST, `payment_header` argument on MCP). Full x402 protocol details: https://x402.org. ### Free dry-run before you pay `POST /journal/directives/validate` (REST) or the `validate_directive` tool (MCP) runs the exact same check a real submission would, against your real history, but never persists anything and never costs money. Use this to catch syntax/balance errors before risking a paid write. ## Part 3 — Endpoints, tools, and pricing | Action | REST | MCP tool | Price | |---|---|---|---| | Register | `POST /register` | `register` | free | | Submit one directive | `POST /journal/directives` | `submit_directive` | $0.0025 (first 20 accepted writes free) | | Dry-run a directive | `POST /journal/directives/validate` | `validate_directive` | free | | Submit up to 1000 directives atomically | `POST /journal/directives/batch` | `submit_batch` | $0.025 (never free, even before any single writes) | | List raw directives | `GET /journal/directives?account=&since=&until=&limit=` | `list_directives` | $0.18 (first 2 calls free) | | List opened accounts | `GET /accounts` | `list_accounts` | $0.02 (first 2 calls free) | | Single account balance | `GET /accounts/balance?account=` | `get_account_balance` | $0.18 (never free) | | Full ledger export (literal Beancount text) | `GET /export` | `export_ledger` | $0.48 (shares a 2-call free pool with the three reports below) | | Balance sheet | `GET /reports/balance-sheet` | `get_balance_sheet` | $0.48 (same shared pool) | | Income statement | `GET /reports/income-statement?since=&until=` | `get_income_statement` | $0.48 (same shared pool) | | Trial balance | `GET /reports/trial-balance?since=&until=` | `get_trial_balance` | $0.48 (same shared pool) | | Health check | `GET /health` | — | free | Free-trial allowances: four **independent** pools, per registered key — using up one has no effect on the others. - **Writes**: your first 20 *accepted* single-directive writes (`submit_directive`/`POST /journal/directives` only — `submit_batch` is never free, even before you've used any single writes at all). An invalid or rejected directive doesn't consume a free write — only genuine, persisted writes count. - **Directive list** (`list_directives`): first 2 calls. - **Account list** (`list_accounts`): first 2 calls. `get_account_balance` is priced the same as `list_directives` but has no free tier at all — don't assume identical pricing means identical free-tier treatment. - **Reports**: `export_ledger`, `get_balance_sheet`, `get_income_statement`, and `get_trial_balance` share ONE combined pool of 2 — not 2 free calls for each of the four. A request rejected for invalid parameters (e.g. `until` before `since`) never consumes a credit from any of these pools, matching how a genuinely paid call in the same situation is verified but never actually settled either. A response includes the relevant `free_*_remaining` field whenever a call was actually free, so you can tell when a given pool is about to run out. Prices above are indicative — **the live 402 response (REST) or `payment_required` result (MCP) at the time of your call is the authoritative price.** `account`/`since`/`until` filters never change a route's price; a filtered call costs the same as an unfiltered one. `GET /accounts/balance` on an account that was never opened still settles at its normal price — an "unknown account" answer is a valid, billable response, not a failed one, and there's no discount for it. ## Part 4 — A complete worked example Track an AI agent's own API spend against a starting balance: ``` 2026-01-01 open Assets:Checking USD 2026-01-01 open Equity:Opening-Balances USD 2026-01-01 open Expenses:API-Costs:Anthropic USD 2026-01-01 open Expenses:API-Costs:OpenAI USD 2026-01-01 * "Starting balance" Assets:Checking 1000.00 USD Equity:Opening-Balances -1000.00 USD 2026-01-15 * "Claude API usage, January" Expenses:API-Costs:Anthropic 42.10 USD Assets:Checking -42.10 USD 2026-01-20 * "GPT API usage, January" Expenses:API-Costs:OpenAI 18.75 USD Assets:Checking -18.75 USD ``` Four `submit_directive` calls (or one `submit_batch` call for all four at once — cheaper per-directive, same $0.025 flat price regardless of count up to 1000). Then `get_balance_sheet` shows `Assets:Checking` at 939.15, and `get_income_statement` shows the API-cost breakdown by provider for whatever period you ask for.