Bookkeeping agent stack
An agent that reconciles Stripe or Paddle revenue against the books in Xero, reads receipts and statements, and prepares draft entries for an accountant. It runs on read-only credentials, and every write needs a human.
Use case
Match payouts and invoices from Stripe or Paddle to what is recorded in Xero each month, explain every difference with the IDs behind it, and draft (never post) the fixes, with read-only credentials and a human approving any write.
Components
| Role | Component | Why |
|---|---|---|
| Agent harness | Claude Agent SDK | Connects the MCP servers and decides which tool calls run. An allowlist plus dontAsk mode lets read tools run without prompts and denies everything else, and a deny rule can hide whole groups of write tools from the model. |
| Accounting ledger | Xero MCP Server | Xero's official local server covers invoices, payments, bank transactions, contacts, the chart of accounts and reports. It has no read-only switch, but it requests exactly the OAuth scopes in XERO_SCOPES, so read-only scopes keep it read-only at Xero's end. |
| Card payments and invoices | Stripe MCP Server | Stripe's hosted server reads payouts, balance transactions, charges, refunds, invoices and subscriptions through one stripe_api_read tool, with writes in a separate stripe_api_write tool you can deny. Agent-tagged restricted keys are subject to Stripe's approval rules. |
| Subscription billing (merchant of record) | Paddle MCP Server | For businesses that sell through Paddle Billing. --tools=read-only loads only read tools, and Paddle API key permissions return 403 for anything the key was not granted. |
| Reconciliation sandbox | E2B | Runs the matching and totals code on exported data, so the model does not do arithmetic in its head. Turn its internet access off once the data is loaded. |
| Receipts and statements | MarkItDown | Turns PDF and Excel statements, receipts and CSV exports into Markdown the agent can read, for documents that are not in any API. |
| Tracing | Langfuse | Keeps every tool call and result, so an accountant can see which Stripe and Xero records each proposed entry came from. |
How the pieces fit
Month-end bookkeeping is mostly matching. Money arrives through Stripe or Paddle, it lands in the bank, and it should show up in Xero as paid invoices and bank transactions. The agent's job is to find what doesn't match and explain why: a fee that wasn't booked, a refund with no credit note, a payout split across two bank lines. An accountant then decides what to post.
- Pull the month's money movements. From Stripe, list payouts, then the balance transactions in each one. The list endpoint takes a
payoutfilter, which works for automatic payouts only. The payout object says so inreconciliation_status, which iscompletedwhen that list is available. Each balance transaction hasamount,feeandnet, wherenetisamountminusfee. From Paddle, list transactions and adjustments (refunds and credits), or read an existing CSV report. - Pull what the books say. From Xero, list invoices, payments, bank transactions (each is marked reconciled or unreconciled), the chart of accounts and tax rates, and a trial balance or P&L for the period.
- Match outside the model. Export both sides to files and run the matching in the E2B sandbox, not in the conversation. The snippet below is a starting point.
- Read the documents that have no API. Bank statement PDFs, supplier receipts and spreadsheets go through MarkItDown.
- Report, then draft. The output is a list of matched items, unmatched items with the IDs on both sides, and proposed entries. Writing those entries is a separate, approved step (see below). Langfuse keeps the trace, so each proposal can be checked against the records it came from.
Keep it read-only
Each of the three servers handles "read-only" differently, so set it up at both ends: in the credential, and in the harness.
- Xero: scopes. The server has no read-only flag; on a test run it listed 51 tools, 25 of which create, update, add, approve, revert or delete. Without
XERO_SCOPES, it asks Xero for its default scope list, which includes read-write scopes such asaccounting.invoicesandaccounting.payments. WhenXERO_SCOPESis set, it requests exactly those scopes and nothing else. The server's README lists the.readversions of the invoice, payment, bank transaction and report scopes. Create the Custom Connection with read scopes only and setXERO_SCOPESto the same list. Xero's FAQ says a call that needs a scope you haven't been granted gets a 401 with an insufficient-scopeWWW-Authenticateheader. Test with a Demo Company first. - Stripe: agent key and approval rules. Stripe recommends OAuth for interactive clients. For an unattended agent, use an agent-tagged restricted key with read permissions on the resources you need. From December 2026, the MCP server stops accepting full-access secret keys and restricted keys without the Agent tag. Agent-tagged keys are subject to approval rules, and Stripe adds default rules for refunds, subscription cancellations and outbound payments. You can add rules (approve or block) for payouts, credit notes, customer balance transactions and more.
- Paddle: tool filter and key permissions. The default
--toolssetting isnon-destructive, and that is not read-only. On a test run,non-destructiveloaded 63 tools, includingcreate_transaction,create_adjustment(refunds and credits) andcreate_subscription_charge.read-onlyloaded 41 tools. Use--tools=read-onlyand give the API key only.readpermissions. Note thatcreate_reportis not a read-only tool. If you want the agent to start new CSV exports, addcreate_reportto an explicit comma-separated tool list (42 tools), or let a person create the report and have the agent read it.
In the harness, auto-approve only read tools and hide the write tools. MCP tools are named mcp__<server>__<tool>. Allow rules accept a glob after the server name, and deny rules both block calls and remove the tool definitions. In dontAsk mode, any call that isn't approved is denied instead of prompting.
import os
from claude_agent_sdk import ClaudeAgentOptions
# Read-only Xero scopes. Give the Custom Connection only these, too.
XERO_READ_SCOPES = " ".join([
"accounting.invoices.read", "accounting.payments.read",
"accounting.banktransactions.read", "accounting.contacts.read",
"accounting.settings.read", "accounting.reports.profitandloss.read",
"accounting.reports.trialbalance.read",
])
options = ClaudeAgentOptions(
mcp_servers={
"xero": {
"command": "npx",
"args": ["-y", "@xeroapi/xero-mcp-server@0.0.17"],
"env": {
"XERO_CLIENT_ID": os.environ["XERO_CLIENT_ID"],
"XERO_CLIENT_SECRET": os.environ["XERO_CLIENT_SECRET"],
"XERO_SCOPES": XERO_READ_SCOPES,
},
},
"stripe": {
"type": "http",
"url": "https://mcp.stripe.com",
# An agent-tagged restricted key with read permissions only
"headers": {"Authorization": f"Bearer {os.environ['STRIPE_AGENT_KEY']}"},
},
},
allowed_tools=[
"mcp__xero__list-*", "mcp__xero__get-*",
"mcp__stripe__stripe_api_search", "mcp__stripe__stripe_api_details",
"mcp__stripe__stripe_api_read",
],
disallowed_tools=[
"mcp__xero__create-*", "mcp__xero__update-*", "mcp__xero__delete-*",
"mcp__xero__approve-*", "mcp__xero__revert-*", "mcp__xero__add-*",
"mcp__stripe__stripe_api_write", "mcp__stripe__send_money",
"mcp__stripe__transfer_money", "mcp__stripe__convert_currency",
],
permission_mode="dontAsk",
)
For Paddle, add a server running npx -y @paddle/paddle-mcp@0.1.6 --tools=read-only with PADDLE_API_KEY and PADDLE_ENVIRONMENT in its env. Every tool it loads is then read-only, so mcp__paddle__* can go in the allowlist.
Writes: drafts first, and a human for each one
When the report is accepted, the fixes can be written, but not by the same unattended run. Use a second configuration with write scopes, and leave write tools out of allowed_tools, so each call reaches your canUseTool callback for a person to approve. That only works in default mode; dontAsk denies the call instead. Know what each Xero write does. In version 0.0.17 of the server:
create-invoice,create-credit-noteandcreate-quotealways create drafts, andcreate-manual-journaldefaults to draft. An accountant can review and approve these in Xero.create-bank-transactioncreates the transaction as authorised, andcreate-paymentrecords a payment against an authorised invoice straight away. Treat these as posting entries.- The server has no tool for bank statement lines or for reconciling them. Matching a statement line in Xero stays a job for a person in Xero itself.
On the Stripe side, writes go through stripe_api_write. Stripe can require human confirmation in the Dashboard for some actions, such as refunds, and your own approval rules can add others or block them outright.
Match payouts outside the model
The snippet below matches Stripe payouts to Xero bank transactions that receive money. A match needs the exact amount and an arrival date within a few days. Anything with zero or several candidates is sent to a person instead of being guessed. Stripe amounts are integers in the currency's minor unit, so it divides by 100. That is only right for two-decimal currencies; zero-decimal currencies such as JPY need no division.
from decimal import Decimal
def match_payouts(payouts, bank_lines, days=3):
"""payouts: Stripe payout dicts (amount in minor units, arrival_date as a date).
bank_lines: Xero RECEIVE bank transactions as dicts with total (Decimal), date and id.
Returns (matches, unmatched_payouts, unmatched_lines). Exact amount, date within +-days."""
free = list(bank_lines)
matches, missing = [], []
for p in sorted(payouts, key=lambda p: p["arrival_date"]):
want = Decimal(p["amount"]) / 100 # two-decimal currencies only
hits = [b for b in free if b["total"] == want
and abs((b["date"] - p["arrival_date"]).days) <= days]
if len(hits) == 1:
matches.append((p["id"], hits[0]["id"]))
free.remove(hits[0])
else:
missing.append((p["id"], f"{len(hits)} candidate lines"))
return matches, missing, [b["id"] for b in free]
For each payout that matches, check the next level down: the payout's balance transactions against the Xero invoices and payments for the same customers. That is where unbooked fees and refunds show up.
Xero's list-bank-transactions tool returns 10 transactions per page as text, newest first. A busy month takes many calls, and every result passes through the model's context. Have the agent page through once, write what it reads to a file, and do the rest of the work in the sandbox.
Test before you connect live books
- Xero: start with the Demo Company, as Xero's README recommends. It comes with sample data and can be reset.
- Stripe: use a sandbox. OAuth consent lets you grant sandboxes and live accounts separate permissions.
- Paddle: set
PADDLE_ENVIRONMENT=sandboxwith a sandbox key. - Keep a month you have already closed by hand as a fixed test case. Rerun the agent on it whenever you change the prompt, the model or a server version, and compare its list of differences with the one your accountant produced.
Variations
- Xero's hosted server. On 2026-10-15 Xero launched a Developer Remote MCP with 39 tools across invoicing, accounting, reporting and payments, for commercial apps on the Core tier or above. The local server above needs no app tier, only a Custom Connection or a bearer token.
- Bank data. The Plaid Dashboard MCP is for diagnosing a Plaid integration (Items, Link conversion, usage), not for reading account transactions, so it is not a bank feed for this stack.
- Other processors. Square, PayPal, Razorpay and Adyen have their own MCP servers; the payment processor MCP servers comparison covers what each can read and write.
- Agents that spend money are a different stack: see Agent that can buy things.
Directory entries in this stack
- Claude Agent SDK: Anthropic's SDK for building production agents with Claude Code as a library, in Python and TypeScript.
- Xero MCP Server: Xero's official local MCP server for its accounting API: contacts, invoices, chart of accounts, payroll and reports.
- Stripe MCP Server: Stripe's hosted remote MCP server giving agents OAuth-secured access to the Stripe API and documentation.
- Paddle MCP Server: Paddle's official MCP server for Paddle Billing: manage products, prices, customers, subscriptions, transactions, refunds and reports.
- E2B: Open-source, secure cloud sandboxes for AI agents: an isolated machine per agent to run code, browse and use tools.
- MarkItDown: Microsoft's lightweight Python utility for converting PDF, Office, images, audio, HTML and more into structure-preserving Markdown for LLMs.
- Langfuse: Open-source agent evals and observability platform: trace, evaluate and improve LLM applications and agents.
- Plaid Dashboard MCP: Plaid's hosted MCP server with Production diagnostics: debug Items, review Link conversion and check usage metrics.
Related
- Payment processor MCP servers compared (Comparison)
- Agent payment protocols compared (Comparison)
- Agent that can buy things (Stack)
- Data analysis agent stack (Stack)
- Agent identity and MCP authorization platforms (Comparison)
- Run agent-generated code safely in a sandbox (Guide)
- Convert documents to Markdown for agents with MarkItDown and Docling (Guide)
- Observability and eval platforms for AI agents (Comparison)
- Add MCP servers to Claude Code (Guide)
Sources
- Xero MCP Server README, accessed
- Xero MCP Server, client credentials and scopes (xero-client.ts), accessed
- Xero Developer changelog, accessed
- Xero granular scopes FAQ, accessed
- Stripe MCP docs, accessed
- Stripe API keys, agent-tagged keys, accessed
- Stripe two-party approvals, accessed
- Stripe API, list balance transactions, accessed
- Stripe API, the payout object, accessed
- Stripe supported currencies, accessed
- Paddle MCP Server README, accessed
- Paddle API key permissions, accessed
- Claude Agent SDK, configure permissions, accessed
- Claude Agent SDK, MCP, accessed
- E2B docs, internet access, accessed
- MarkItDown README, accessed
- Langfuse README, accessed
- Plaid Dashboard MCP docs, accessed