Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Skillagentskills.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 suggestion field telling the caller how to fix it.
  • Trivially deployable – copy one binary, run it. The SQLite .db file is your entire database.

Tech Stack

ComponentChoice
LanguageRust
Web FrameworkAxum
DatabaseSQLite 3 (rusqlite, bundled + i128_blob)
Connection Pooldeadpool-sqlite
Migrationsrefinery (embedded SQL, forward-only)
FrontendSvelteKit SPA (adapter-static) + Tailwind CSS + shadcn-svelte
API Docsutoipa + swagger-ui
AuthAPI keys (agents) + JWT (web users)
CLIclap

Installation

Prerequisites

  • Rust (edition 2024)
  • pnpm (only needed for frontend development)

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:

VariableDefaultDescription
CLAWCOUNTING_DB./clawcounting.dbSQLite database file path
CLAWCOUNTING_PORT3000HTTP server port
CLAWCOUNTING_JWT_SECRETAuto-generatedJWT signing secret. Auto-generated and stored in DB if not set.
CLAWCOUNTING_API_KEYAPI 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_key immediately – 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_KEY set 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 TypeDebit increasesCredit increases
AssetBalance goes upBalance goes down
ExpenseBalance goes upBalance goes down
LiabilityBalance goes downBalance goes up
EquityBalance goes downBalance goes up
RevenueBalance goes downBalance goes up

Mnemonic: Assets and Expenses are “debit-normal” – they increase with debits. Everything else is “credit-normal.”

Account Types and Normal Balances

TypeNormal BalanceEquation SideClosed at Period End?
assetdebitLeft (A)No – permanent
liabilitycreditRight (L)No – permanent
equitycreditRight (E)No – permanent
revenuecreditIncome StatementYes – zeroed to retained earnings
expensedebitIncome StatementYes – 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:

  1. Balanced – total debits must equal total credits
  2. Minimum 2 lines – at least one debit and one credit
  3. Same currency – all lines must reference accounts with the same currency
  4. Open period – the entry date must fall within an open financial period
  5. Active accounts – all referenced accounts must be active
  6. No control accounts – cannot post directly to accounts with has_subledger=true
  7. 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:

CurrencyAsset Scale1.0 stored as$10.50 stored as
USD21001050
JPY01N/A
BTC8100000000N/A
ETH181000000000000000000N/A
USDC61000000N/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:

TypePatternExample
Fiat (ISO 4217)swift:0/iso4217:<CODE>swift:0/iso4217:USD
Native coin (ETH)eip155:<chain>/slip44:<coin>eip155:1/slip44:60
ERC-20 tokeneip155:<chain>/erc20:<address>eip155:1/erc20:0xa0b8...
Bitcoinbip122:<genesis>/slip44:0bip122: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

  • code is unique and immutable after creation
  • caip19_id is unique and immutable after creation
  • asset_scale is immutable (changing it would corrupt all existing balances)
  • Only name and symbol can 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:

  1. Entry in USD: Debit “Currency Exchange” clearing account, Credit USD Cash
  2. 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_date must be before end_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:

  1. Looks up which period contains the entry_date
  2. If no period covers that date – error (NO_OPEN_PERIOD)
  3. If the matching period is closed – error (PERIOD_CLOSED)
  4. The entry is linked to that period via period_id

Period Close

Closing a period is a permanent operation that:

  1. Debits each revenue account to zero its balance for the period
  2. Credits each expense account to zero its balance for the period
  3. Posts the net income (or net loss) to the retained earnings account
  4. Creates an automatic closing journal entry linked to the period
  5. Marks the period as closed with a timestamp

Prerequisites

  • retained_earnings_account_id must 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=true and acts as a summary.
  • Sub-accounts have parent_id pointing to the control account and an entity_id identifying 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_id and entity_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 AccountEntity IDPurpose
Accounts ReceivableCustomer IDTrack amounts owed by each customer
Accounts PayableVendor IDTrack amounts owed to each vendor
InventoryProduct SKUTrack inventory by item
Fixed AssetsAsset tag/serialTrack 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 name and symbol
  • Account name, is_active, and xbrl_tag
  • User name, permissions, and is_active
  • Settings values

How to Correct Mistakes

Since journal entries cannot be edited or deleted, corrections are made through reversing entries:

  1. Reverse the incorrect entry – this creates a new entry with all debits and credits swapped
  2. 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=true and reverses_id pointing 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_KEY env var or --api-key flag 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:

  1. Metadata (~100 tokens) – name and description, loaded at startup for activation matching
  2. Instructions (<5,000 tokens) – full SKILL.md body, loaded when the agent does accounting work
  3. References (on demand) – detailed guides loaded only when specific procedures are needed

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

  1. Check setup – verify currencies, accounts, and periods exist
  2. Post entries – create journal entries for transactions
  3. Query state – check balances, run reports
  4. Handle errors – read error suggestions, adjust and retry
  5. 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

OptionDescription
--db <path>Database file path (default: ./clawcounting.db or CLAWCOUNTING_DB env var)
--jsonMachine-readable JSON output
--api-key <key>API key for write operations (or set CLAWCOUNTING_API_KEY env var)
--versionPrint version
--helpPrint 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:

CommandAPI Key Required
currencies list/createNo
accounts list/create/getNo
periods list/create/getNo
reports *No
users list/create/getNo
settings get/setNo
journal-entries createYes
journal-entries reverseYes
periods closeYes

CLI / API Equivalents

CLI CommandAPI Endpoint
currencies create ...POST /api/v1/currencies
currencies listGET /api/v1/currencies
currencies create-fiat USDN/A (CLI convenience)
accounts create ...POST /api/v1/accounts
accounts listGET /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-balanceGET /api/v1/reports/trial-balance
reports balance-sheetGET /api/v1/reports/balance-sheet
reports income-statementGET /api/v1/reports/income-statement
reports general-ledgerGET /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:

MethodHeaderUse case
API KeyAuthorization: Bearer tsk_...Service accounts (agents)
JWTAuthorization: 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"
}
FieldTypeRequiredNotes
codestringyesUnique, e.g. “USD”, “ETH”
namestringyesDisplay name
symbolstringyese.g. “$”, “Ξ”
asset_scaleintegeryesDecimal places (2 for cents, 18 for wei)
asset_typestringyes"fiat" or "crypto"
caip19_idstringyesUnique 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"
}
FieldTypeRequiredNotes
account_numberstringyesUnique identifier (e.g. “1000”)
namestringyesDisplay name
currency_idstringyesMust reference an existing currency
account_typestringyesasset, liability, equity, revenue, expense
normal_balancestringyesdebit or credit
has_subledgerboolnoEnable sub-accounts (default: false)
parent_idstringnoParent control account (for sub-accounts)
entity_idstringnoEntity identifier (required with parent_id)
xbrl_tagstringnoXBRL classification tag

List Accounts

GET /api/v1/accounts?account_type=asset&currency_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:

FieldTypeRequiredNotes
entry_datestringyesISO 8601 date, must be in an open period
descriptionstringyesEntry description
referencestringnoExternal reference (invoice number, etc.)
metadataobjectnoArbitrary JSON metadata
linesarrayyesMinimum 2 lines

Line fields:

FieldTypeRequiredNotes
account_idstringyesMust be active, same currency as other lines
debit_amountstringconditionali128 in smallest unit. Exactly one of debit/credit per line.
credit_amountstringconditionali128 in smallest unit. Exactly one of debit/credit per line.
descriptionstringnoLine-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>&currency_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

CodeHTTP StatusMeaningRecovery
VALIDATION_ERROR400Field validation failedRead field and suggestion – fix the specific field
UNBALANCED_ENTRY400Total debits != total creditsCheck line amounts. Ensure they sum to equal values.
PERIOD_CLOSED409Period is closedPost to a different open period
NOT_FOUND404Resource doesn’t existVerify the ID. List resources to find the correct one.
UNAUTHORIZED401Missing or invalid authCheck Authorization header. Re-login or use valid API key.
FORBIDDEN403Insufficient permissionsCheck user permissions. Request access from admin.
DATABASE_ERROR500Database failureRetry. If persistent, check disk space and database integrity.
INTERNAL_ERROR500Unexpected errorRetry. 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_amount values and all credit_amount values – 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_date doesn’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_id before posting

“Retained earnings account not configured”

  • Period close requires retained_earnings_account_id in 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 balance
  • foreign_keys = ON – enforce referential integrity
  • busy_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().await queue 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 to SUM() for i128 BLOBs
  • i128_add(a, b) – scalar addition of two i128 BLOBs
  • i128_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

  1. Open a raw rusqlite::Connection
  2. Set pragmas (WAL, foreign keys, synchronous, busy_timeout)
  3. Register custom SQLite functions (i128_add, sum_i128, i128_to_text)
  4. Run refinery embedded migrations
  5. Branch:
    • Server: Close bootstrap connection, create deadpool pools (with custom functions registered on each connection via post_create hook), start Axum server
    • CLI: Keep the connection, execute the requested command, exit