Introduction
ClawCounting is a foundational double-entry bookkeeping engine designed for AI agents. It ships as a single Rust binary backed by a single SQLite database file.
It is a platform, not an end-user application – it provides the primitives (currencies, accounts, journal entries, periods, balances, reports) that more complex systems are built on top of. Consuming systems – whether AI agents, ERPs, invoicing systems, or custom business logic – use the API or CLI to build domain-specific workflows (AR/AP, payroll, tax, reconciliation, approvals, etc.).
Key Features
- Double-entry accounting – every journal entry must balance (total debits == total credits), with minimum 2 lines, all in the same currency
- Immutable journal – journal entries are append-only; corrections via reversing entries only
- Financial periods – fiscal year management with permanent close, automatic closing entries
- Subledger support – control accounts with per-entity sub-accounts (AR/AP by customer/vendor)
- Multi-currency – fiat (ISO 4217) and crypto (ERC-20, native coins) via CAIP-19 identifiers, with full wei-precision (i128 amounts)
- Two interfaces – REST API (for agents and web UI) and CLI (for scripts, cron, admin) sharing the same service layer
- Built-in web UI – SvelteKit SPA embedded in the binary
- OpenAPI docs – auto-generated spec with Swagger UI
- Agent Skill – agentskills.io standard skill for teaching AI agents accounting workflows
- Zero deployment overhead – no Docker, no external database servers, no runtime dependencies
Design Philosophy
- Single-tenant – each deployment serves one tenant. Multi-tenant = separate instances.
- Server and CLI share the same service layer – identical validation, business logic, and transactions regardless of how you interact with it.
- The API enforces correctness – balanced entries, period rules, subledger constraints. The less computation delegated to the calling agent, the fewer errors.
- Structured errors with recovery guidance – every error includes a
suggestionfield telling the caller how to fix it. - Trivially deployable – copy one binary, run it. The SQLite
.dbfile is your entire database.
Tech Stack
| Component | Choice |
|---|---|
| Language | Rust |
| Web Framework | Axum |
| Database | SQLite 3 (rusqlite, bundled + i128_blob) |
| Connection Pool | deadpool-sqlite |
| Migrations | refinery (embedded SQL, forward-only) |
| Frontend | SvelteKit SPA (adapter-static) + Tailwind CSS + shadcn-svelte |
| API Docs | utoipa + swagger-ui |
| Auth | API keys (agents) + JWT (web users) |
| CLI | clap |
Installation
Prerequisites
Build from Source
# Clone and install
git clone https://github.com/johnkozan/clawcounting.git
cd clawcounting
cargo install --path .
# Verify
clawcounting --version
Quick Start
# Initialize the database and start the server
clawcounting init
clawcounting serve
The server starts at http://localhost:3000. On first run, you’ll be guided through setup (creating your first user) via the web UI.
Configuration
ClawCounting works out of the box with sensible defaults – no configuration file needed. You can optionally override settings via environment variables or a .env file:
| Variable | Default | Description |
|---|---|---|
CLAWCOUNTING_DB | ./clawcounting.db | SQLite database file path |
CLAWCOUNTING_PORT | 3000 | HTTP server port |
CLAWCOUNTING_JWT_SECRET | Auto-generated | JWT signing secret. Auto-generated and stored in DB if not set. |
CLAWCOUNTING_API_KEY | – | API key for CLI write operations. Alternative to --api-key flag. |
A .env.example file is included in the repository for reference.
What Gets Created
Running clawcounting init creates:
- The SQLite database file (default:
./clawcounting.db) - All tables, indexes, triggers, and migrations
- A JWT secret (stored in the settings table)
Running clawcounting serve starts:
- REST API at
/api/v1/* - Web UI at
/ - Swagger API docs at
/swagger-ui/ - Health check at
/health
Setup Guide
Complete these steps in order after installation.
1. Initialize the Database
# Initialize in the default location (./clawcounting.db)
clawcounting init
# Or specify a path
clawcounting init --db /path/to/clawcounting.db
Running init on an existing database is safe – it will apply any pending migrations.
2. Choose Your Interface
CLI only (no server needed):
clawcounting currencies list --json
Server mode (REST API + web UI):
clawcounting serve
# => Listening on 0.0.0.0:3000
Both interfaces share the same database and can be used simultaneously.
3. Create a Service Account
Service accounts authenticate with API keys. This is the recommended auth method for AI agents and CLI write operations.
clawcounting users create-service-account --name "AI Agent" --json
{
"data": {
"user": {
"id": "019...",
"name": "AI Agent",
"permissions": {},
"is_active": true
},
"api_key": "tsk_a1b2c3d4..."
}
}
Save the
api_keyimmediately – it is shown only once.
Set it as an environment variable for convenience:
export CLAWCOUNTING_API_KEY=tsk_a1b2c3d4...
4. Add Currencies
Every account requires a currency. Add currencies before creating accounts.
Web UI
Click Add Currency on the Currencies page. The dialog lets you:
- Fiat – pick from a searchable list of ISO 4217 currencies (with country flags)
- Crypto > Popular – pick from built-in native chains (BTC, ETH, SOL, etc.) and ~400 ERC-20 tokens (with logos)
- Crypto > Import List – import tokens from a Uniswap Token List JSON (URL or file upload) with a preview and bulk-add
- Crypto > Custom – manually enter any currency
CLI
# Fiat (auto-fills name, symbol, asset_scale, CAIP-19)
clawcounting currencies create-fiat USD --json
# Crypto (manual)
clawcounting currencies create \
--code ETH \
--name "Ether" \
--symbol "Ξ" \
--asset-scale 18 \
--type crypto \
--caip19 "eip155:1/slip44:60" \
--json
See Currencies & Amounts for details on CAIP-19 identifiers and asset scales.
5. Create a Financial Period
Every journal entry must fall within an open financial period.
clawcounting periods create \
--name "FY2026" \
--start 2026-01-01 \
--end 2026-12-31 \
--json
Periods must not overlap. They can be any duration (month, quarter, year). See Financial Periods for more.
6. Build Chart of Accounts
Create accounts for the types of transactions you’ll record:
# Get your currency ID
CURRENCY_ID=$(clawcounting currencies list --json | jq -r '.data[0].id')
# Asset accounts
clawcounting accounts create --name "Cash" --currency $CURRENCY_ID \
--type asset --normal-balance debit --number 1000 --json
clawcounting accounts create --name "Accounts Receivable" --currency $CURRENCY_ID \
--type asset --normal-balance debit --number 1200 --json
# Liability accounts
clawcounting accounts create --name "Accounts Payable" --currency $CURRENCY_ID \
--type liability --normal-balance credit --number 2000 --json
# Equity accounts
clawcounting accounts create --name "Owner's Equity" --currency $CURRENCY_ID \
--type equity --normal-balance credit --number 3000 --json
clawcounting accounts create --name "Retained Earnings" --currency $CURRENCY_ID \
--type equity --normal-balance credit --number 3100 --json
# Revenue accounts
clawcounting accounts create --name "Service Revenue" --currency $CURRENCY_ID \
--type revenue --normal-balance credit --number 4000 --json
# Expense accounts
clawcounting accounts create --name "Rent Expense" --currency $CURRENCY_ID \
--type expense --normal-balance debit --number 5000 --json
clawcounting accounts create --name "Wages Expense" --currency $CURRENCY_ID \
--type expense --normal-balance debit --number 5100 --json
7. Configure Retained Earnings
Period close requires knowing which equity account to transfer net income into:
RE_ID=$(clawcounting accounts list --type equity --json | \
jq -r '.data[] | select(.name == "Retained Earnings") | .id')
clawcounting settings set retained-earnings-account $RE_ID
Setup Checklist
- Database initialized (
clawcounting init) - At least one currency created
- At least one open financial period
- Chart of accounts with asset, liability, equity, revenue, and expense accounts
- Retained earnings account configured in settings
- Service account created with API key (for CLI write ops and API access)
-
CLAWCOUNTING_API_KEYset in environment
Double-Entry Bookkeeping
Every financial transaction in ClawCounting is recorded as a journal entry with two or more lines. The fundamental rule:
Total debits must equal total credits in every journal entry.
An unbalanced entry is rejected with error code UNBALANCED_ENTRY.
Debits and Credits
| Account Type | Debit increases | Credit increases |
|---|---|---|
| Asset | Balance goes up | Balance goes down |
| Expense | Balance goes up | Balance goes down |
| Liability | Balance goes down | Balance goes up |
| Equity | Balance goes down | Balance goes up |
| Revenue | Balance goes down | Balance goes up |
Mnemonic: Assets and Expenses are “debit-normal” – they increase with debits. Everything else is “credit-normal.”
Account Types and Normal Balances
| Type | Normal Balance | Equation Side | Closed at Period End? |
|---|---|---|---|
asset | debit | Left (A) | No – permanent |
liability | credit | Right (L) | No – permanent |
equity | credit | Right (E) | No – permanent |
revenue | credit | Income Statement | Yes – zeroed to retained earnings |
expense | debit | Income Statement | Yes – zeroed to retained earnings |
The accounting equation: Assets = Liabilities + Equity
ClawCounting verifies this equation in the balance sheet report (is_balanced field).
Permanent vs Temporary Accounts
- Permanent (asset, liability, equity): Balances carry forward across periods.
- Temporary (revenue, expense): Balances are zeroed when a period is closed, with net income transferred to retained earnings.
Examples
Receive cash for services rendered:
- Debit Cash (asset goes up)
- Credit Revenue (revenue goes up)
Pay rent:
- Debit Rent Expense (expense goes up)
- Credit Cash (asset goes down)
Take out a loan:
- Debit Cash (asset goes up)
- Credit Loan Payable (liability goes up)
Multi-line entry – payroll with tax withholding:
- Debit Wages Expense $5,000
- Credit Tax Payable $750
- Credit Cash $4,250
Total debits ($5,000) = total credits ($750 + $4,250). The entry balances.
Validation Rules
ClawCounting enforces these rules on every journal entry:
- Balanced – total debits must equal total credits
- Minimum 2 lines – at least one debit and one credit
- Same currency – all lines must reference accounts with the same currency
- Open period – the entry date must fall within an open financial period
- Active accounts – all referenced accounts must be active
- No control accounts – cannot post directly to accounts with
has_subledger=true - Non-negative amounts – each line has either a debit or credit amount, not both, and it must be positive
Currencies & Amounts
ClawCounting supports both fiat and crypto currencies with full precision.
Amount Representation
All monetary amounts are stored as i128 integers in the smallest currency unit:
- No floating point – zero rounding errors
- Maximum value: ±170,141,183,460,469,231,731,687,303,715,884,105,727
- Stored as 16-byte BLOBs in SQLite
The asset_scale on each currency defines decimal placement:
| Currency | Asset Scale | 1.0 stored as | $10.50 stored as |
|---|---|---|---|
| USD | 2 | 100 | 1050 |
| JPY | 0 | 1 | N/A |
| BTC | 8 | 100000000 | N/A |
| ETH | 18 | 1000000000000000000 | N/A |
| USDC | 6 | 1000000 | N/A |
Formula: stored = display × 10^asset_scale
In API Requests
Amounts are sent as string representations of the i128 value in the smallest unit:
{"debit_amount": "1050"}
Not "10.50" – always the raw integer.
In API Responses
Both forms are provided:
{
"debit_amount": "1050",
"display_debit": "10.50"
}
CAIP-19 Identifiers
Every currency has a unique CAIP-19 identifier – a chain-agnostic standard that gives every asset a single canonical identifier:
| Type | Pattern | Example |
|---|---|---|
| Fiat (ISO 4217) | swift:0/iso4217:<CODE> | swift:0/iso4217:USD |
| Native coin (ETH) | eip155:<chain>/slip44:<coin> | eip155:1/slip44:60 |
| ERC-20 token | eip155:<chain>/erc20:<address> | eip155:1/erc20:0xa0b8... |
| Bitcoin | bip122:<genesis>/slip44:0 | bip122:000000000019d6689c085ae165831e93/slip44:0 |
Web UI
The Add Currency dialog in the web UI provides the easiest way to add currencies:
- Fiat tab – searchable picker of all ISO 4217 currencies with country flags. Click to add.
- Crypto tab with three sub-tabs:
- Popular – searchable picker of native chains (BTC, ETH, SOL, etc.) and ~400 ERC-20 tokens from the Uniswap Default token list, with logos. Click to add.
- Import List – import tokens from any Uniswap Token List standard JSON. Paste a URL or upload a file, preview the tokens with checkboxes, and bulk-add your selection.
- Custom – manual form for any currency not in the built-in lists.
Logos are resolved client-side: fiat currencies show country flags (derived from the ISO 4217 code), crypto currencies show token logos from the built-in data.
CLI
Fiat Currencies
Use create-fiat with an ISO 4217 code – ClawCounting auto-fills everything:
clawcounting currencies create-fiat USD --json
Crypto Currencies
Provide all fields manually:
clawcounting currencies create \
--code ETH \
--name "Ether" \
--symbol "Ξ" \
--asset-scale 18 \
--type crypto \
--caip19 "eip155:1/slip44:60" \
--json
Currency Constraints
codeis unique and immutable after creationcaip19_idis unique and immutable after creationasset_scaleis immutable (changing it would corrupt all existing balances)- Only
nameandsymbolcan be updated
Multi-Currency Rules
- All lines in a single journal entry must reference accounts with the same currency
- Cross-currency transactions require separate entries or a clearing account pattern
- Balance and report queries can filter by
currency_id
Cross-Currency Pattern
To record a currency exchange (e.g., sell USD for EUR), use two separate entries with a clearing account:
- Entry in USD: Debit “Currency Exchange” clearing account, Credit USD Cash
- Entry in EUR: Debit EUR Cash, Credit “Currency Exchange” clearing account
Exchange rate metadata can be stored in the journal entry’s metadata JSON field.
Multi-Chain Tokens
The same token on different chains is tracked as separate currencies. USDC on Ethereum and USDC on Solana are distinct currencies with distinct CAIP-19 IDs and distinct codes (e.g., “USDC-ETH” vs “USDC-SOL”). This is correct for accounting – they have different settlement properties and may have different valuations.
Financial Periods
Financial periods define the date ranges for accounting activity. Every journal entry must fall within an open period.
Rules
start_datemust be beforeend_date- Periods must not overlap
- Periods can be any duration (day, month, quarter, year)
- Gaps between periods are allowed
- Multiple periods can be open simultaneously
Open vs Closed
- Open: Accepts new journal entries within its date range.
- Closed: Permanently sealed. No new entries, no reopening.
Entry Date Validation
When posting a journal entry, ClawCounting:
- Looks up which period contains the
entry_date - If no period covers that date – error (
NO_OPEN_PERIOD) - If the matching period is closed – error (
PERIOD_CLOSED) - The entry is linked to that period via
period_id
Period Close
Closing a period is a permanent operation that:
- Debits each revenue account to zero its balance for the period
- Credits each expense account to zero its balance for the period
- Posts the net income (or net loss) to the retained earnings account
- Creates an automatic closing journal entry linked to the period
- Marks the period as closed with a timestamp
Prerequisites
retained_earnings_account_idmust be set in settings- The retained earnings account must be an equity-type account
- The period must be open
Workflow
Always preview first:
# Preview -- shows the closing entry without committing
clawcounting periods close <period-id> --preview --api-key $API_KEY --json
# Close -- permanent
clawcounting periods close <period-id> --api-key $API_KEY --json
After Closing
- No new entries can be posted to the closed period
- The period cannot be reopened
- Errors in closed periods are corrected via adjusting entries in an open period
- Asset, liability, and equity balances carry forward naturally (they are permanent accounts)
Examples
Annual periods
clawcounting periods create --name "FY2026" --start 2026-01-01 --end 2026-12-31 --json
Quarterly periods
clawcounting periods create --name "Q1 2026" --start 2026-01-01 --end 2026-03-31 --json
clawcounting periods create --name "Q2 2026" --start 2026-04-01 --end 2026-06-30 --json
clawcounting periods create --name "Q3 2026" --start 2026-07-01 --end 2026-09-30 --json
clawcounting periods create --name "Q4 2026" --start 2026-10-01 --end 2026-12-31 --json
Subledgers
Subledgers provide detail within a single line item on the trial balance – for example, tracking receivables per customer or payables per vendor.
How It Works
- A control account has
has_subledger=trueand acts as a summary. - Sub-accounts have
parent_idpointing to the control account and anentity_ididentifying the counterparty. - The control account balance is computed as the sum of all sub-account balances.
- You post to sub-accounts, never directly to the control account.
Example: Accounts Receivable
Accounts Receivable (control, has_subledger=true)
├── AR - Acme Corp (entity_id: acme) balance: $3,000
├── AR - Beta Inc (entity_id: beta) balance: $1,500
└── AR - Gamma LLC (entity_id: gamma) balance: $500
Control account balance (computed): $5,000
A journal entry for a sale to Acme Corp:
{
"entry_date": "2026-03-15",
"description": "Invoice #1001 to Acme Corp",
"lines": [
{"account_id": "<ar-acme-sub-account-id>", "debit_amount": "250000"},
{"account_id": "<revenue-id>", "credit_amount": "250000"}
]
}
Setup
# Create control account with subledger flag
clawcounting accounts create \
--name "Accounts Receivable" \
--currency $CURRENCY_ID \
--type asset \
--normal-balance debit \
--number 1200 \
--subledger \
--json
# Create sub-accounts per entity
clawcounting accounts create \
--name "AR - Acme Corp" \
--number 1200-001 \
--parent $AR_ID \
--entity "acme-corp" \
--json
Sub-accounts inherit currency_id, account_type, and normal_balance from the parent – you don’t need to specify them.
Constraints
- Control accounts (
has_subledger=true) cannot be posted to directly - Sub-accounts require both
parent_idandentity_id - The parent must have
has_subledger=true - A control account cannot itself be a sub-account
- Sub-accounts inherit currency, type, and normal balance from the parent
Querying
# Control account balance = sum of all sub-accounts
curl http://localhost:3000/api/v1/accounts/<control-id>/balance
# List sub-accounts
curl http://localhost:3000/api/v1/accounts/<control-id>/sub-accounts
# Individual sub-account balance
curl http://localhost:3000/api/v1/accounts/<sub-account-id>/balance
Common Patterns
| Control Account | Entity ID | Purpose |
|---|---|---|
| Accounts Receivable | Customer ID | Track amounts owed by each customer |
| Accounts Payable | Vendor ID | Track amounts owed to each vendor |
| Inventory | Product SKU | Track inventory by item |
| Fixed Assets | Asset tag/serial | Track individual fixed assets |
Immutability & Corrections
ClawCounting enforces strict immutability on accounting records to maintain a complete audit trail.
What Cannot Be Changed
- Journal entries – no UPDATE, no DELETE. Once posted, the entry exists permanently.
- Journal entry lines – append-only.
- Closed periods – cannot be reopened.
- Currency code, caip19_id, and asset_scale – immutable after creation.
- Account type, normal_balance, and currency_id – immutable after creation.
What Can Be Changed
- Currency
nameandsymbol - Account
name,is_active, andxbrl_tag - User
name,permissions, andis_active - Settings values
How to Correct Mistakes
Since journal entries cannot be edited or deleted, corrections are made through reversing entries:
- Reverse the incorrect entry – this creates a new entry with all debits and credits swapped
- Post a new entry with the correct amounts
Both the original error and the correction remain in the ledger, providing a complete audit trail.
Example
# 1. Reverse the wrong entry
clawcounting journal-entries reverse <wrong-entry-id> --api-key $API_KEY --json
# 2. Post the corrected entry
clawcounting journal-entries create --file corrected-entry.json --api-key $API_KEY --json
Reversal Rules
- The reversal date must fall within an open period
- A reversal cannot itself be reversed
- The original entry is not modified – it remains in the ledger
- After reversal, the net effect on all accounts is zero
- The reversal entry has
is_reversal=trueandreverses_idpointing to the original
Why Immutability?
This design follows standard accounting practice:
- Audit trail – every change is visible and traceable
- Data integrity – no risk of accidentally or maliciously altering historical records
- Regulatory compliance – many jurisdictions require immutable financial records
- Simplicity – the balance trigger only needs to handle INSERTs, never UPDATEs or DELETEs
Operations Guide
Day-to-day accounting operations with ClawCounting.
Posting Journal Entries
Journal entries are the core of double-entry bookkeeping. Every entry must balance: total debits = total credits.
From a JSON File (CLI)
Create entry.json:
{
"entry_date": "2026-03-15",
"description": "Monthly office rent",
"reference": "INV-2026-0042",
"metadata": {"vendor": "Acme Properties"},
"lines": [
{
"account_id": "<rent-expense-account-id>",
"debit_amount": "150000",
"description": "March 2026 rent"
},
{
"account_id": "<cash-account-id>",
"credit_amount": "150000",
"description": "Payment for March rent"
}
]
}
Post it:
clawcounting journal-entries create --file entry.json --json
Requires
CLAWCOUNTING_API_KEYenv var or--api-keyflag for user attribution.
Via API
curl -X POST http://localhost:3000/api/v1/journal-entries \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"entry_date": "2026-03-15",
"description": "Monthly office rent",
"lines": [
{"account_id": "<rent-expense-id>", "debit_amount": "150000"},
{"account_id": "<cash-id>", "credit_amount": "150000"}
]
}'
Multi-Line Entries
Entries can have more than 2 lines. Example – payroll with tax withholding:
{
"entry_date": "2026-03-31",
"description": "March payroll",
"lines": [
{"account_id": "<wages-expense-id>", "debit_amount": "500000"},
{"account_id": "<tax-payable-id>", "credit_amount": "75000"},
{"account_id": "<cash-id>", "credit_amount": "425000"}
]
}
Debits (500000) = Credits (75000 + 425000). Entry balances.
Querying Balances
# All-time balance for an account
clawcounting accounts get <account-id> --json
# Period-specific balance (API)
curl "http://localhost:3000/api/v1/accounts/<id>/balance?period_id=<period-id>" \
-H "Authorization: Bearer $API_KEY"
Reversing Entries
Reversals are the only way to correct a posted journal entry. A reversal creates a new entry with all debits and credits swapped.
# Reverse with original date
clawcounting journal-entries reverse <entry-id> --json
# Reverse with a different date
clawcounting journal-entries reverse <entry-id> --date 2026-04-01 --json
After reversal, the net effect on all accounts is zero. Then post a new corrected entry.
Generating Reports
Trial Balance
Verifies that total debits equal total credits across all accounts.
clawcounting reports trial-balance --json
clawcounting reports trial-balance --period <period-id> --json
Balance Sheet
Shows assets, liabilities, and equity. Verifies Assets = Liabilities + Equity.
clawcounting reports balance-sheet --json
clawcounting reports balance-sheet --as-of 2026-06-30 --json
Income Statement
Shows revenue, expenses, and net income for a period.
clawcounting reports income-statement --period <period-id> --json
General Ledger
Detailed transaction history for a single account with running balance.
clawcounting reports general-ledger \
--account <account-id> \
--start 2026-01-01 \
--end 2026-03-31 \
--json
Closing a Financial Period
See Financial Periods for the full process. The short version:
# Always preview first
clawcounting periods close <period-id> --preview --api-key $API_KEY --json
# Then close permanently
clawcounting periods close <period-id> --api-key $API_KEY --json
Common Patterns
Record a sale
{
"entry_date": "2026-03-15",
"description": "Sale to customer",
"reference": "INV-001",
"lines": [
{"account_id": "<cash-or-ar-id>", "debit_amount": "100000"},
{"account_id": "<revenue-id>", "credit_amount": "100000"}
]
}
Record an expense
{
"entry_date": "2026-03-15",
"description": "Office supplies",
"lines": [
{"account_id": "<supplies-expense-id>", "debit_amount": "5000"},
{"account_id": "<cash-id>", "credit_amount": "5000"}
]
}
Transfer between accounts
{
"entry_date": "2026-03-15",
"description": "Transfer from checking to savings",
"lines": [
{"account_id": "<savings-id>", "debit_amount": "1000000"},
{"account_id": "<checking-id>", "credit_amount": "1000000"}
]
}
Record a loan payment (principal + interest)
{
"entry_date": "2026-03-15",
"description": "Monthly loan payment",
"lines": [
{"account_id": "<loan-payable-id>", "debit_amount": "80000"},
{"account_id": "<interest-expense-id>", "debit_amount": "20000"},
{"account_id": "<cash-id>", "credit_amount": "100000"}
]
}
Agent Integration
ClawCounting is designed for AI agent consumption. This guide covers how to integrate an AI agent with ClawCounting.
Agent Skill
ClawCounting includes an Agent Skill in skill/SKILL.md. The skill teaches AI agents how to use the accounting engine – domain rules, workflows, and best practices.
Supported platforms: Claude Code, Claude.ai, VS Code/Copilot, Cursor, OpenAI Codex, Gemini CLI, JetBrains Junie, and others.
The skill follows a progressive disclosure model:
- Metadata (~100 tokens) – name and description, loaded at startup for activation matching
- Instructions (<5,000 tokens) – full SKILL.md body, loaded when the agent does accounting work
- References (on demand) – detailed guides loaded only when specific procedures are needed
CLI Interface (Recommended)
The CLI is the recommended interface for AI agents. It connects directly to the SQLite database with no server needed.
# Always use --json for machine-readable output
clawcounting currencies list --json
clawcounting accounts list --json
clawcounting reports trial-balance --json
Authentication for Write Operations
Commands that create accounting records (journal entries, reversals, period close) require an API key:
# Via environment variable (recommended)
export CLAWCOUNTING_API_KEY=tsk_...
clawcounting journal-entries create --file entry.json --json
# Via flag
clawcounting journal-entries create --file entry.json --api-key tsk_... --json
Admin commands (user/currency/account/period creation, reports, settings) work without an API key.
HTTP API
For agents that communicate over HTTP:
curl -H "Authorization: Bearer tsk_..." \
http://localhost:3000/api/v1/accounts --json
See API Reference for all endpoints.
Starting the Server in the Background
When an agent needs to start the server as part of a workflow:
# Start server in background
nohup clawcounting serve > /tmp/clawcounting.log 2>&1 &
echo $! > /tmp/clawcounting.pid
# Wait for readiness
until curl -sf http://localhost:3000/health > /dev/null 2>&1; do sleep 0.5; done
# Server is now accepting requests
To stop:
kill "$(cat /tmp/clawcounting.pid)"
Error Handling
Errors follow RFC 7807 and include a suggestion field with recovery guidance:
{
"code": "PERIOD_CLOSED",
"message": "Period FY2025 is closed",
"field": null,
"suggestion": "Post to period FY2026 which is currently open"
}
Always read the suggestion before retrying. See Error Codes for the full list.
Response Envelope
Single resource:
{ "data": { ... } }
List (cursor-paginated):
{ "data": [...], "has_more": true, "next_cursor": "..." }
Pagination: ?limit=50&cursor=<next_cursor>. Default limit 50, max 200.
OpenAPI Schema
The full OpenAPI spec is available at /swagger-ui when the server is running. Agents can use the schema for endpoint discovery and parameter validation.
Typical Agent Workflow
- Check setup – verify currencies, accounts, and periods exist
- Post entries – create journal entries for transactions
- Query state – check balances, run reports
- Handle errors – read error suggestions, adjust and retry
- Period management – preview and close periods when appropriate
CLI Reference
ClawCounting’s CLI provides direct access to all accounting operations. It connects directly to the SQLite database – no server needed.
Global Options
| Option | Description |
|---|---|
--db <path> | Database file path (default: ./clawcounting.db or CLAWCOUNTING_DB env var) |
--json | Machine-readable JSON output |
--api-key <key> | API key for write operations (or set CLAWCOUNTING_API_KEY env var) |
--version | Print version |
--help | Print help |
Commands
Database
clawcounting init # Initialize database (create file, run migrations)
clawcounting serve # Start HTTP server + web UI
Currencies
clawcounting currencies list # List all currencies
clawcounting currencies create-fiat USD # Create fiat from ISO 4217 code
clawcounting currencies create \
--code ETH \
--name "Ether" \
--symbol "Ξ" \
--asset-scale 18 \
--type crypto \
--caip19 "eip155:1/slip44:60" # Create custom currency
Accounts
clawcounting accounts list # List all accounts
clawcounting accounts list --type asset # Filter by type
clawcounting accounts get <id> # Get account details + balance
clawcounting accounts create \
--name "Cash" \
--currency <currency-id> \
--type asset \
--normal-balance debit \
--number 1000 # Create account
clawcounting accounts create \
--name "AR - Acme" \
--number 1200-001 \
--parent <control-account-id> \
--entity "acme-corp" # Create sub-account
Journal Entries
clawcounting journal-entries list # List entries
clawcounting journal-entries list --period <period-id> # Filter by period
clawcounting journal-entries get <id> # Get entry with lines
clawcounting journal-entries create --file entry.json # Create from JSON file
clawcounting journal-entries reverse <id> # Reverse an entry
clawcounting journal-entries reverse <id> --date 2026-04-01 # Reverse with new date
Financial Periods
clawcounting periods list # List all periods
clawcounting periods get <id> # Get period details
clawcounting periods create \
--name "FY2026" \
--start 2026-01-01 \
--end 2026-12-31 # Create period
clawcounting periods close <id> --preview # Preview closing entry
clawcounting periods close <id> # Close permanently
Reports
clawcounting reports trial-balance # All-time trial balance
clawcounting reports trial-balance --period <id> # Period-specific
clawcounting reports balance-sheet # Current balance sheet
clawcounting reports balance-sheet --as-of 2026-06-30 # As of date
clawcounting reports balance-sheet --period <id> # Through end of period
clawcounting reports income-statement --period <id> # Income for period
clawcounting reports general-ledger \
--account <id> \
--start 2026-01-01 \
--end 2026-03-31 # Account detail
Users
clawcounting users list # List all users
clawcounting users get <id> # Get user details
clawcounting users create \
--name "Admin" \
--email "admin@example.com" \
--password "secure" # Create human user
clawcounting users create-service-account \
--name "AI Agent" # Create service account (returns API key)
Settings
clawcounting settings get # Show all settings
clawcounting settings set retained-earnings-account <id> # Set retained earnings account
API Key Requirements
Commands that create accounting records require an API key for user attribution:
| Command | API Key Required |
|---|---|
currencies list/create | No |
accounts list/create/get | No |
periods list/create/get | No |
reports * | No |
users list/create/get | No |
settings get/set | No |
journal-entries create | Yes |
journal-entries reverse | Yes |
periods close | Yes |
CLI / API Equivalents
| CLI Command | API Endpoint |
|---|---|
currencies create ... | POST /api/v1/currencies |
currencies list | GET /api/v1/currencies |
currencies create-fiat USD | N/A (CLI convenience) |
accounts create ... | POST /api/v1/accounts |
accounts list | GET /api/v1/accounts |
journal-entries create --file ... | POST /api/v1/journal-entries |
journal-entries reverse <id> | POST /api/v1/journal-entries/{id}/reverse |
periods create ... | POST /api/v1/periods |
periods close <id> | POST /api/v1/periods/{id}/close |
reports trial-balance | GET /api/v1/reports/trial-balance |
reports balance-sheet | GET /api/v1/reports/balance-sheet |
reports income-statement | GET /api/v1/reports/income-statement |
reports general-ledger | GET /api/v1/reports/general-ledger |
settings set ... | PATCH /api/v1/settings |
API Reference
Complete REST API reference. Base URL: http://localhost:3000.
Authentication
All /api/v1/* endpoints require authentication:
| Method | Header | Use case |
|---|---|---|
| API Key | Authorization: Bearer tsk_... | Service accounts (agents) |
| JWT | Authorization: Bearer eyJ... | Human users (web UI) |
Login (JWT)
POST /auth/login
{
"email": "admin@example.com",
"password": "secret"
}
Response:
{
"access_token": "eyJ...",
"refresh_token": "eyJ...",
"token_type": "bearer",
"expires_in": 900
}
Refresh Token
POST /auth/refresh
{
"refresh_token": "eyJ..."
}
Current User
GET /auth/me
Currencies
Create Currency
POST /api/v1/currencies
{
"code": "USD",
"name": "United States Dollar",
"symbol": "$",
"asset_scale": 2,
"asset_type": "fiat",
"caip19_id": "swift:0/iso4217:USD"
}
| Field | Type | Required | Notes |
|---|---|---|---|
code | string | yes | Unique, e.g. “USD”, “ETH” |
name | string | yes | Display name |
symbol | string | yes | e.g. “$”, “Ξ” |
asset_scale | integer | yes | Decimal places (2 for cents, 18 for wei) |
asset_type | string | yes | "fiat" or "crypto" |
caip19_id | string | yes | Unique CAIP-19 identifier |
List Currencies
GET /api/v1/currencies?limit=50&cursor=<cursor>
Get Currency
GET /api/v1/currencies/{id}
Update Currency
PATCH /api/v1/currencies/{id}
Only name and symbol are updatable.
Accounts
Create Account
POST /api/v1/accounts
{
"account_number": "1000",
"name": "Cash",
"currency_id": "019...",
"account_type": "asset",
"normal_balance": "debit"
}
| Field | Type | Required | Notes |
|---|---|---|---|
account_number | string | yes | Unique identifier (e.g. “1000”) |
name | string | yes | Display name |
currency_id | string | yes | Must reference an existing currency |
account_type | string | yes | asset, liability, equity, revenue, expense |
normal_balance | string | yes | debit or credit |
has_subledger | bool | no | Enable sub-accounts (default: false) |
parent_id | string | no | Parent control account (for sub-accounts) |
entity_id | string | no | Entity identifier (required with parent_id) |
xbrl_tag | string | no | XBRL classification tag |
List Accounts
GET /api/v1/accounts?account_type=asset¤cy_id=...&is_active=true&parent_id=...&limit=50&cursor=...
Get Account
GET /api/v1/accounts/{id}
Update Account
PATCH /api/v1/accounts/{id}
Only name, is_active, and xbrl_tag are updatable.
Get Sub-Accounts
GET /api/v1/accounts/{id}/sub-accounts
Get Account Balance
GET /api/v1/accounts/{id}/balance?period_id=<optional>
Without period_id, returns cumulative balance across all periods. For control accounts, returns sum of all sub-account balances.
Get Account Transactions
GET /api/v1/accounts/{id}/transactions?limit=50&cursor=...
Journal Entries
Create Journal Entry
POST /api/v1/journal-entries
{
"entry_date": "2026-03-15",
"description": "Monthly rent payment",
"reference": "INV-001",
"metadata": {"vendor": "Acme"},
"lines": [
{
"account_id": "019...",
"debit_amount": "150000",
"description": "Rent expense"
},
{
"account_id": "019...",
"credit_amount": "150000",
"description": "Cash payment"
}
]
}
Entry fields:
| Field | Type | Required | Notes |
|---|---|---|---|
entry_date | string | yes | ISO 8601 date, must be in an open period |
description | string | yes | Entry description |
reference | string | no | External reference (invoice number, etc.) |
metadata | object | no | Arbitrary JSON metadata |
lines | array | yes | Minimum 2 lines |
Line fields:
| Field | Type | Required | Notes |
|---|---|---|---|
account_id | string | yes | Must be active, same currency as other lines |
debit_amount | string | conditional | i128 in smallest unit. Exactly one of debit/credit per line. |
credit_amount | string | conditional | i128 in smallest unit. Exactly one of debit/credit per line. |
description | string | no | Line-level description |
List Journal Entries
GET /api/v1/journal-entries?period_id=...&start_date=...&end_date=...&account_id=...&limit=50&cursor=...
Get Journal Entry
GET /api/v1/journal-entries/{id}
Returns entry with all lines, including display_debit and display_credit formatted amounts.
Reverse Journal Entry
POST /api/v1/journal-entries/{id}/reverse
{
"entry_date": "2026-04-01"
}
entry_date is optional – defaults to original entry date. The reversal date must fall within an open period. A reversal cannot be reversed.
Financial Periods
Create Period
POST /api/v1/periods
{
"name": "FY2026",
"start_date": "2026-01-01",
"end_date": "2026-12-31"
}
Periods must not overlap with existing periods.
List Periods
GET /api/v1/periods?limit=50&cursor=...
Get Period
GET /api/v1/periods/{id}
Close Period
POST /api/v1/periods/{id}/close?preview=true
With preview=true: returns the closing entry without committing. Without preview: closes the period permanently.
Reports
Trial Balance
GET /api/v1/reports/trial-balance?period_id=<optional>¤cy_id=<optional>
Returns account rows with debit/credit totals and is_balanced flag.
Balance Sheet
GET /api/v1/reports/balance-sheet?period_id=<optional>&as_of_date=<optional>
Returns sections: assets, liabilities, equity. Includes is_balanced (Assets = Liabilities + Equity).
Income Statement
GET /api/v1/reports/income-statement?period_id=<optional>
Returns: revenue, expenses, total_revenue, total_expenses, net_income.
General Ledger
GET /api/v1/reports/general-ledger?account_id=<required>&period_id=<optional>&start_date=<optional>&end_date=<optional>&sort=desc&limit=50&cursor=...
Returns: starting_balance, paginated lines with running_balance, and ending_balance.
Settings
Get Settings
GET /api/v1/settings
Update Settings
PATCH /api/v1/settings
{
"retained_earnings_account_id": "019..."
}
Users
Create Human User
POST /api/v1/users
{
"name": "Admin",
"email": "admin@example.com",
"password": "secure-password",
"permissions": {}
}
Create Service Account
POST /api/v1/users/service-accounts
{
"name": "AI Agent",
"permissions": {}
}
Response includes api_key (shown only once).
List Users
GET /api/v1/users?limit=50&cursor=...
Get User
GET /api/v1/users/{id}
Update User
PATCH /api/v1/users/{id}
{
"name": "New Name",
"permissions": {"journals:create": true},
"is_active": false
}
Error Codes
All errors follow RFC 7807 (Problem Details) and include a suggestion field with recovery guidance.
Error Response Format
{
"code": "UNBALANCED_ENTRY",
"message": "Journal entry does not balance: debits=1050, credits=1000",
"field": null,
"suggestion": "Adjust line amounts so total debits equal total credits"
}
Always check the suggestion field – it provides specific guidance for recovery.
Error Code Reference
| Code | HTTP Status | Meaning | Recovery |
|---|---|---|---|
VALIDATION_ERROR | 400 | Field validation failed | Read field and suggestion – fix the specific field |
UNBALANCED_ENTRY | 400 | Total debits != total credits | Check line amounts. Ensure they sum to equal values. |
PERIOD_CLOSED | 409 | Period is closed | Post to a different open period |
NOT_FOUND | 404 | Resource doesn’t exist | Verify the ID. List resources to find the correct one. |
UNAUTHORIZED | 401 | Missing or invalid auth | Check Authorization header. Re-login or use valid API key. |
FORBIDDEN | 403 | Insufficient permissions | Check user permissions. Request access from admin. |
DATABASE_ERROR | 500 | Database failure | Retry. If persistent, check disk space and database integrity. |
INTERNAL_ERROR | 500 | Unexpected error | Retry. Report if persistent. |
Common Mistakes and Fixes
“Entry does not balance”
- Double-check all amounts are in the smallest unit (cents, not dollars)
- Verify you haven’t mixed up debit and credit on a line
- Sum all
debit_amountvalues and allcredit_amountvalues – they must be equal
“Cannot post to control account”
- The account has
has_subledger=true - Find or create a sub-account under it and post there instead
- List sub-accounts:
GET /api/v1/accounts/{id}/sub-accounts
“No open period for date”
- The
entry_datedoesn’t fall within any period, or the matching period is closed - Create a new period covering that date, or change the entry date
- List periods:
clawcounting periods list --json
“Currency mismatch”
- All accounts in a journal entry must share the same currency
- Check each account’s
currency_idbefore posting
“Retained earnings account not configured”
- Period close requires
retained_earnings_account_idin settings - Set it:
clawcounting settings set retained-earnings-account <id> - The account must be an equity-type account
Architecture
Overview
Browser / Swagger UI / AI Agents (HTTP)
|
clawcounting serve (port 3000)
/api/v1/... -> API routes
/swagger-ui -> OpenAPI docs
/* -> SvelteKit SPA
|
services/ layer (shared business logic)
|
AI Agents / Scripts (CLI)
|
SQLite (WAL mode, single .db file)
Both the HTTP server and CLI share the same services/ layer – identical validation, business logic, and transaction handling. The only difference is the entry point (HTTP handlers vs CLI commands) and output format (JSON vs text tables).
Single-Tenant Design
Strictly single-tenant. Each deployment (binary + .db file) serves one tenant. Multi-tenant is achieved by running separate instances, each with its own database file. This eliminates org_id from all tables, simplifies every query, and removes org-scoping from routes.
SQLite Configuration
- WAL mode (Write-Ahead Logging) – allows concurrent readers during writes
synchronous = NORMAL– good durability/performance balanceforeign_keys = ON– enforce referential integritybusy_timeout = 5000– wait up to 5 seconds for write lock
Connection Strategy
Server Mode (deadpool-sqlite)
- Write pool (size 1) – serializes all writes within the server process. Handlers calling
write_pool.get().awaitqueue up for the one connection. - Read pool (size 2-4) – multiple read-only connections for concurrent queries.
- All DB work runs inside
interact()closures on blocking threads – Tokio async workers are never blocked.
This mirrors SQLite’s “single writer, multiple readers” concurrency model under WAL mode.
CLI Mode (direct rusqlite)
Opens a single rusqlite::Connection directly – no pool needed for a short-lived process.
Cross-Process (server + CLI simultaneously)
Both processes may write to the same .db file. SQLite’s file-level locking handles this. busy_timeout = 5000 on every connection gives a 5-second window for lock contention. In practice, write transactions are short (milliseconds), so collisions are rare.
Amount Storage
All monetary amounts are stored as i128 integers in 16-byte BLOBs. The encoding (MSB-flipped big-endian) makes memcmp() produce correct signed ordering, so SQLite’s ORDER BY, <, >, MIN, MAX all work correctly.
Custom SQLite functions are registered on every connection:
sum_i128(column)– aggregate equivalent toSUM()for i128 BLOBsi128_add(a, b)– scalar addition of two i128 BLOBsi128_to_text(column)– converts BLOB to decimal string for debugging
Balance Updates
A SQLite trigger (update_balance_on_insert) on journal_entry_lines automatically maintains per-period account_balances using i128_add(). The trigger fires within the same transaction as the journal entry insert – no application-layer read-modify-write needed.
CREATE TRIGGER update_balance_on_insert
AFTER INSERT ON journal_entry_lines
BEGIN
INSERT INTO account_balances (account_id, period_id, total_debits, total_credits)
VALUES (
NEW.account_id,
(SELECT period_id FROM journal_entries WHERE id = NEW.journal_entry_id),
NEW.debit_amount,
NEW.credit_amount
)
ON CONFLICT(account_id, period_id) DO UPDATE SET
total_debits = i128_add(total_debits, NEW.debit_amount),
total_credits = i128_add(total_credits, NEW.credit_amount);
END;
Transaction Safety
All write operations use BEGIN IMMEDIATE to acquire the write lock at transaction start (not deferred until the first write). This prevents race conditions – for example, two concurrent period close requests both reading the period as open before either commits.
Project Structure
src/
main.rs # Entrypoint, clap dispatch
config.rs # DB path, port, env vars (CLAWCOUNTING_*)
app_state.rs # Shared state (connection pools)
error.rs # AppError, RFC 7807 responses
router.rs # Route assembly
db/
connection.rs # SQLite setup (WAL, pragmas, custom functions)
i128_funcs.rs # Custom SQLite functions
pool.rs # deadpool-sqlite pools
migrations.rs # refinery embedded migrations
middleware/auth.rs # API key + JWT validation
models/ # Request/response structs
handlers/ # Axum route handlers
services/ # Business logic (shared between HTTP and CLI)
cli/ # CLI command handlers
migrations/ # SQL migration files
skill/ # Agent Skill (agentskills.io standard)
frontend/ # SvelteKit SPA
tests/ # Integration tests
Startup Flow
- Open a raw
rusqlite::Connection - Set pragmas (WAL, foreign keys, synchronous, busy_timeout)
- Register custom SQLite functions (
i128_add,sum_i128,i128_to_text) - Run
refineryembedded migrations - Branch:
- Server: Close bootstrap connection, create deadpool pools (with custom functions registered on each connection via
post_createhook), start Axum server - CLI: Keep the connection, execute the requested command, exit
- Server: Close bootstrap connection, create deadpool pools (with custom functions registered on each connection via