An autonomous OODA-loop agent (Observe → Orient → Decide → Act → Learn) that connects to a real Xero organisation via the official Xero MCP Server, finds revenue-protecting opportunities in real invoice/payment data, and drafts real Xero objects for a human to approve — never sending, authorising, or moving money on its own.
Built for the "Bring Your Own AI Agent" Xero hackathon bounty: actively identify and act on revenue opportunities using Xero data — data analysis + autonomous action, turning insights into measurable business outcomes.
Works with Xero. Not an official Xero product.
- Observe — pulls 6 real data sources from Xero in parallel (bank accounts, aged receivables, aged payables, 12-month P&L, contact history, payment patterns) and streams each one live over SSE as it resolves.
- Orient — two deterministic detectors turn that data into hypotheses, each with real evidence and a transparent, inspectable confidence formula (no LLM, no black box):
- Dormant high-value customer — top-15%-by-revenue contact gone silent past 2× their usual ordering rhythm
- Late-payment risk — open invoices whose contact's historical payment lag projects past the due date
- Decide — every hypothesis becomes a proposed action sitting in a human approval queue. Nothing executes without a click.
- Act — approving a
draft_quoteaction creates a real DRAFT quote in Xero (verified live, deep link included). Approving adraft_followupproduces drafted text for a human to copy and send — Xero's MCP server doesn't send email, so the agent doesn't pretend to either. - Learn — a live summary (cash risk defended, revenue identified, approvals still pending) computed from what was actually approved or rejected, plus a timestamped action log.
Everything downstream of Observe is computed from the same real dataset — nothing in this app is scripted or faked, and every honest gap (see below) is labeled as such in the UI rather than papered over.
(Agent Thinking tab — live OODA trace on real Xero data, Dashboard, Actions Timeline)
Add your own screenshots to a docs/ folder and reference them here, e.g.:
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND — React + Vite + TS + Tailwind (client/) │
│ • Agent Thinking tab — live OODA trace via SSE │
│ • Dashboard — real cash-flow projection + risk tables │
│ • Actions Timeline — approval queue │
└───────────────▲─────────────────────────────────────┬────────────┘
│ GET /api/health, /api/stream │ POST /api/run
│ │ POST /api/actions/:id/approve|reject
┌───────────────┴─────────────────────────────────────▼────────────┐
│ BACKEND — Node + TypeScript + Fastify (server/) │
│ observe.ts → orient.ts → decide.ts → act.ts │
│ xero/mcpClient.ts — the only module that talks to Xero │
└───────────────────────────────┬────────────────────────────────────┘
│ MCP over stdio
┌────────────▼────────────┐
│ @xeroapi/xero-mcp-server │
└────────────┬────────────┘
│ OAuth2 (Custom Connection)
Xero Accounting API
- Backend: Node 20+, TypeScript, Fastify,
@modelcontextprotocol/sdk(MCP client) - Frontend: React 19, Vite, Tailwind CSS v4, Recharts, Framer Motion, lucide-react
- Xero integration:
@xeroapi/xero-mcp-servervia the Model Context Protocol - Auth: Xero Custom Connection (machine-to-machine, single org)
This project prioritizes never fabricating a number. Where the live API can't return something, the UI says so instead of guessing:
| Feature | Status |
|---|---|
| 6 Observe data sources | ✅ Real, live |
| Dormant-customer / late-payment detectors | ✅ Real, computed from live invoices |
| Draft quote creation in Xero | ✅ Real write, verified live, DRAFT status only |
| Approval queue (approve/reject) | ✅ Real, backed by the write above |
| Cash health score | ✅ Real formula (DSO, receivables/payables coverage, late-payment risk) |
| Available cash (absolute balance) | ✅ Real, from list-report-balance-sheet's Bank section |
| 90-day cash flow chart | ✅ Real starting balance + scheduled receivables/payables, with the £4,000 safety-threshold line |
| Opportunities matrix | Rendered as a ranked table rather than a 2×2 scatter plot |
See server/xero/NOTES.md for the full list of real-world API discrepancies discovered while building this (report-grid shapes, a hard periods cap on P&L, locale-dependent date strings, no idempotency-key field on writes, etc.) — written up as they were found, against live data, not assumed from docs.
- Every write is a DRAFT. Never
AUTHORISED, nevercreate-payment, never anything that moves money. - Every action requires human approval — nothing executes automatically.
- No email is ever sent by the agent — Xero's MCP server can't send email, so "drafted follow-up" text is exactly that: text for a human to copy.
- No Xero secret ever reaches the browser bundle.
Full human setup checklist: see QUICKSTART.md (Xero Demo Company → Custom Connection app → scopes → .env).
# 1. Install dependencies
npm install
cd client && npm install && cd ..
# 2. Configure
cp .env.example .env # fill in XERO_CLIENT_ID / XERO_CLIENT_SECRET / ANTHROPIC_API_KEY
# 3. Run the backend (http://localhost:8787)
npm run dev
# 4. Run the frontend (http://localhost:5173), in a second terminal
cd client && npm run devSet DEMO_MODE=true in .env to run entirely offline against captured real-data fixtures (server/xero/__fixtures__/) — useful for demos where live API access or rate limits are a risk. Every fixture was captured from a real Xero Demo Company, not hand-written.
accounting.reports.aged.read
accounting.reports.profitandloss.read
accounting.reports.balancesheet.read
accounting.reports.trialbalance.read
accounting.reports.banksummary.read
accounting.contacts.read
accounting.settings.read
accounting.banktransactions.read
accounting.invoices
accounting.invoices.read
The scopes actually requested are whatever
XERO_SCOPESin.envlists — it's an override, not just a filter on what the Xero app is authorized for. If a report call fails with a generic error, check this variable before assuming the Xero app itself is missing a scope (seeserver/xero/NOTES.md§5b — this exact mix-up cost real debugging time).
server/
index.ts Fastify app — health, run, SSE stream, approval endpoints
observe.ts 6 real Observe data sources
orient.ts Dormant-customer + late-payment detectors
decide.ts Hypothesis → proposed action mapping
act.ts Executes approved actions (real Xero writes)
xero/
mcpClient.ts The only module that talks to the Xero MCP server
parsers.ts Turns Xero's text/report responses into typed data
types.ts Shared Xero data types
NOTES.md Real API discrepancies found during development
__fixtures__/ Captured real responses, reused for DEMO_MODE
client/
src/
components/ Xero-branded UI (Agent Thinking / Dashboard / Actions Timeline)
hooks/useOodaRun.ts SSE client + approval-queue state
lib/ API client, cash-flow/health-score calculations


