Building on an Open Source Swedish Ledger
Accounted is AGPL-licensed Swedish double-entry bookkeeping: 123 REST endpoints, 130+ MCP tools, 482 structured error codes, and a Docker self-host path. This is the developer tour — the stack, how to run it yourself, how API keys and scopes work, and what the validation layer refuses to let an agent do.
TL;DRAccounted is AGPL-licensed Swedish double-entry bookkeeping: 123 REST endpoints, 130+ MCP tools, 482 structured error codes, and a Docker self-host path. This is the developer tour — the stack, how to run it yourself, how API keys and scopes work, and what the validation layer refuses to let an agent do.
This is the developer entry point if you want to build with accounted rather than evaluate it from outside. Self-hosting, the validation layer, how MCP and REST relate, and what you get out of the box. It assumes you've read The Accounting API for the AI Era once.
The stack
No exotic choices. That's deliberate — the interesting part of an accounting system should be the accounting, not the infrastructure.
┌──────────────────────────────────────────────────────┐
│ Clients (Web UI / Claude / Cursor / your backend) │
└──────────────────────────────────────────────────────┘
│ │
│ REST │ MCP
▼ ▼
┌──────────────────────────────────────────────────────┐
│ Next.js 16 route handlers (App Router) │
│ - API-key scopes, OAuth 2.1, idempotency cache │
└──────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Deterministic validation layer │
│ - Debit/credit balance │
│ - BAS chart + account existence │
│ - Fiscal period state machine │
│ - Swedish VAT rules │
└──────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────┐
│ Postgres (Supabase) — RLS, triggers, pg_cron │
│ Append-only event log + journal entries │
└──────────────────────────────────────────────────────┘
Next.js 16, React 19, Postgres via Supabase. Everything that writes goes through validation; the event log records what happened.
Self-hosting
Prerequisites: Docker with Compose v2, and a Supabase project (the free tier works).
git clone https://github.com/erp-mafia/gnubok.git
cd gnubok
./setup.sh
setup.sh walks you through the environment configuration. You'll need the project URL, anon key, and service-role key from your Supabase project's Settings → API.
Then apply the schema:
npm install -g supabase
supabase link --project-ref <your-project-ref>
supabase db push
The migrations enable uuid-ossp, pgvector (AI embeddings), btree_gist (fiscal period overlap prevention), and pg_cron (scheduled jobs). pg_cron needs a paid Supabase plan — on the free tier that migration fails, and you skip it. The cron sidecar container does the same work over HTTP instead.
Two things to know about self-hosted deployments: the Docker image sets NEXT_PUBLIC_SELF_HOSTED=true, which disables MFA enforcement (users can still enable TOTP themselves), and you should configure your own SMTP provider rather than relying on Supabase's built-in email rate limits.
The published image is ghcr.io/erp-mafia/gnubok:latest. Full instructions live in docs/SELF-HOSTING.md in the repo.
API keys and scopes
Keys are minted in the dashboard at /settings/api. They're prefixed gnubok_sk_test_* for sandbox and gnubok_sk_live_* for real data.
Scopes are per capability, not per role. There is no admin scope that grants everything. A sample:
| Scope | Grants |
|---|---|
transactions:read | List transactions, template and category suggestions |
transactions:write | Categorise, uncategorise, receipt matching, invoice linking |
reports:read | Chart of accounts, ledgers, balance sheet, VAT, KPI, SIE |
bookkeeping:write | Close/lock periods, opening balances, year-end, SIE import |
skatteverket:write | File VAT and AGI (stages; signed with BankID) |
pending_operations:approve | Approve or reject staged operations |
webhooks:manage | Create, list, update, delete webhook subscriptions |
A key created with no explicit scopes gets a read-only default set. OAuth-issued keys are read-only by default too — every write scope has to be requested by the client and affirmatively ticked by the user on the consent screen.
Segregation of duties is enforced. You cannot mint one key holding both a staging scope and pending_operations:approve. That combination would let an automated agent stage and commit a posting with no human review, which defeats the approval model and doesn't satisfy BFL 5 kap 5 § behandlingshistorik.
What the validation layer refuses
Every write is checked before anything commits:
- Balance — total debits equal total credits.
- Account existence — every line's account is in the chart of accounts.
- Period state — the target period is open. Locked periods reject writes.
- VAT consistency — VAT treatment matches account type; reverse-charge cases flagged.
- Idempotency — a repeated
idempotency_keyreturns the original result instead of double-booking. 118 of 123 endpoints are idempotent.
Failures come back as one of 482 structured error codes, each carrying a machine-readable code, a recovery hint, and a docs URL. That last part matters more than it sounds: an agent that gets recovery_hint back can usually fix its own call without a human reading the error.
55 endpoints also support dry-run, so you can validate a write without performing it. Not all of them — check the endpoint's docs rather than assuming.
The MCP tool surface
130+ tools, discovered automatically when a client connects. Grouped by domain:
- Transactions — list, categorise, match against invoices, receipt matching.
- Invoices — customer ledger, create, send, mark paid, credit.
- Supplier invoices — inbox processing, approval, crediting.
- Reports — trial balance, income statement, balance sheet, KPI, dimension P&L.
- VAT — report generation, close checks, declaration filing.
- Periods — open, lock, close, year-end orchestration.
- Payroll — salary runs, payslips, AGI generation, vacation liability.
- Audit and migration — SIE import/export, audit package, voucher gap explanations.
Rather than memorising them, call gnubok_search_tools to rank capabilities by query, or gnubok_list_skills and gnubok_load_skill to get step-by-step workflows for things like month-end close or a VAT declaration.
Pending operations
Every write through MCP stages a pending operation rather than committing. The shape:
{
"operation_id": "po_2026_04829",
"type": "categorize_transaction",
"risk_level": "low",
"preview": {
"transaction_id": "t_2026_19182",
"proposed_account": "6212",
"proposed_vat": "25%"
},
"requires_approval": true
}
You approve in chat or at /pending. Operations carry a risk level (low, medium, high) that reflects what's at stake — categorising a transaction is not the same as locking a period or booking a salary run.
Worth being precise about one asymmetry: staged writes are MCP-only. The REST endpoint POST /api/v1/companies/{companyId}/transactions/{id}/categorize creates the journal entry directly. If you're writing backend code, you own the review step yourself; the staging model is there for agent access, where the human isn't reading every call.
Agent auto-commit was removed in May 2026. There is no configuration that books an agent's proposal without a human approving it.
REST and MCP
Which one you use depends on who's driving:
- REST for code you write. Scheduled jobs, webhook handlers, your own product features.
- MCP for LLM-driven work. Claude, Cursor, agents that orchestrate multi-step operations.
Both hit the same validation layer and produce the same audit trail. Neither is a second-class surface.
There are no official TypeScript or Python SDKs — the API is plain REST with an OpenAPI 3.1 spec at https://app.accounted.se/api/v1/openapi.json, and llms.txt / llms-full.txt if you'd rather point a coding agent at it and let it generate the client.
What you do next
Evaluating? Mint a gnubok_sk_test_* key and connect Claude in read-only mode. Twenty minutes to see what agentic access to a ledger actually feels like.
Integrating? Read the API documentation, issue a scoped key, and start with GET /api/v1/companies. Build one small thing that works before expanding.
Embedding? Contact us via /byraer for commercial licensing. See Embedded accounting for SaaS for the integration shape and the current provisioning gap.
Contributing? Open an issue on GitHub. Non-trivial changes benefit from agreeing the direction first.
The accounting category needed an API-first contender. The interesting thing isn't accounted itself — it's what becomes possible once bookkeeping is treated like any other piece of backend infrastructure: open, scriptable, and made of primitives instead of products.
Frequently asked
- Why AGPL instead of MIT?
- AGPL keeps the core from being forked, hosted as a SaaS, and resold without contribution. Commercial licences are available for products that embed accounted inside proprietary software. The MCP server package is MIT — it's glue, not the protected substrate.
- What database does it need?
- Postgres, via Supabase. The schema, RLS policies, triggers, and functions all ship as ordered migrations in supabase/migrations. Some migrations need pgvector, btree_gist, and pg_cron. There is no SQLite path.
- How does the validation layer work?
- Every write runs through deterministic checks before commit: debits equal credits, accounts exist, the target period is open, VAT treatment is consistent, idempotency key is unseen. Failures return one of 482 structured error codes with a recovery hint. No LLM sits in the validation path.
- Can an agent book something without me?
- Not through MCP. Every MCP write stages a pending operation that you approve in chat or at /pending. A single credential can't both stage and approve — that combination is rejected at the authorization layer.
- How do I contribute?
- Open an issue on GitHub for bugs and feature requests, and open one before starting non-trivial work so we can align on direction. PRs welcome, tests required for new functionality.
Last updated: July 26, 2026