bitwave — agent-first accounting, local or cloud
bitwave is an agent-first accounting platform: AI agents and humans keep
complete, auditable, double-entry books through one CLI. Every command is
non-interactive, every output is parseable, and every action is
balance-checked — so an agent can drive the books end-to-end and a human
can audit every step.
It runs locally against plain-text journal files in the same format as ledger-cli, hledger, and (with a syntax shim) beancount — your books stay yours, readable by every tool in the plain-text-accounting ecosystem. When more than one person — or agent — needs the books, any workspace can be shared or persisted in Bitwave's cloud general ledger with the exact same commands.
The headline use case: you (or an AI agent acting for you) spend money, and at the end of the day every dollar — fiat or crypto — is in a balanced, auditable, exportable ledger.
$ bitwave init --name acme-2026
$ bitwave je new --date 2026-05-29 --payee "AWS" \
--posting "Expenses:Cloud $42.18" \
--posting "Assets:Checking -$42.18"
$ bitwave bal
$42.18 Expenses:Cloud
-$42.18 Assets:Checking
$ bitwave je export | hledger -f - bal # any other PTA tool reads it
Why bitwave
- Agent-first ergonomics. Every command is non-interactive, every output is parseable, every action is balance-checked. An agent's work lands as replayable commands and diffable files. See Agents, please read.
- Real double-entry. Every entry must balance before it's written. Catches drift the moment you make it, not at month-end.
- Local or cloud, same surface. Same commands work whether the
data lives in a directory of
.journalfiles or in a cloudLedgerWorkspace. Flip a TOML field to switch. - Plain text, your repo. Every transaction is a line in a
.journalfile. Diff it, blame it, branch it, commit it. No SQLite blob, no API lock-in. - Cross-tool compatible. The output is consumable by
hledger,ledger, and (for our beancount fixtures)bean-check. The compatibility suite that proves it lives in thebitwave-accounting-sdk. - Crypto-native, optional. Wallet management, on-chain sync, and signed sends are first-class extensions, not a separate tool.
Install
Homebrew (macOS / Linux)
brew install bitwave-io/tap/bitwave
npm
npm install -g bitwave
Shell (macOS / Linux)
curl -fsSL https://cli.bitwave.io/install.sh | sh
Installs the latest release to ~/.local/bin after verifying its
checksum. Set BITWAVE_VERSION=v0.x.y / BITWAVE_INSTALL_DIR=... to
override.
Go
go install github.com/bitwave-io/bitwave-cli/cmd/bitwave@latest
From source
git clone https://github.com/bitwave-io/bitwave-cli
cd bitwave-cli
make bitwave
sudo install -m 755 bitwave /usr/local/bin/bitwave
You'll need Go 1.25+.
Quickstart — local workspace
# 1. Scaffold a workspace in the current directory.
$ bitwave init --name personal-2026 --base-currency USD
created .bitwave.toml
# 2. Declare a couple of accounts.
$ bitwave acct add Assets:Checking
$ bitwave acct add Expenses:Groceries
$ bitwave acct add Income:Salary
# 3. Record income.
$ bitwave je new \
--date 2026-05-01 --payee "Acme Corp" \
--posting "Assets:Checking $5000.00" \
--posting "Income:Salary -$5000.00"
# 4. Record an expense.
$ bitwave je new \
--date 2026-05-03 --payee "Trader Joe's" \
--posting "Expenses:Groceries $124.36" \
--posting "Assets:Checking -$124.36"
# 5. Look at the books.
$ bitwave bal
$4875.64 Assets:Checking
$124.36 Expenses:Groceries
-$5000.00 Income:Salary
$ bitwave reg
2026-05-01 Acme Corp Assets:Checking $5000.00 $5000.00
Income:Salary -$5000.00 0
2026-05-03 Trader Joe's Expenses:Groceries $124.36 $124.36
Assets:Checking -$124.36 0
That's the whole loop. Everything else is variations on it.
Quickstart — interop with hledger / ledger / beancount
bitwave is a polite citizen of the plain-text-accounting world.
# Take a workspace built with bitwave and run hledger reports on it.
$ bitwave je export > all.journal
$ hledger -f all.journal bal
$ hledger -f all.journal incomestatement
# Import a journal someone wrote in hledger or ledger.
$ bitwave je import their-2025.journal
imported 412 entries into journal `default`
# Import a beancount file. Cost-basis annotations `{...}` and `@`/`@@`
# prices are preserved.
$ bitwave je import their-2025.beancount
imported 187 entries into journal `default`
What works in both directions:
| Feature | Read | Write |
|---|---|---|
Standard transactions (*/! status) |
||
@ unit-price, @@ total-price |
||
| Multi-currency / multi-commodity | ||
ISO YYYY-MM-DD, slash, dot dates |
(ISO out) | |
include / !include directives |
inlined | |
Transaction (CODE) field |
||
; # * comments |
; out |
|
| Elided posting amount (inferred) | always explicit | |
Beancount open/close/price/commodity |
(ledger-format out) |
The exhaustive matrix lives in the bitwave-accounting-sdk.
Cloud mode
Local workspaces are just files. Cloud workspaces are the same surface, backed by Bitwave's cloud general ledger.
# Sign in (PKCE browser flow).
$ bitwave auth login
# Pick an org, or create one.
$ bitwave org list
$ bitwave org create --name "My Company"
$ bitwave org use <org-id>
# Create a cloud-backed workspace in this org.
$ bitwave init --cloud --name "FY2026"
created cloud workspace ws_abc123 in org org_xyz789
# Same commands, same outputs — data lives server-side now.
$ bitwave je new --date 2026-05-29 --payee "AWS" \
--posting "Expenses:Cloud $42.18" \
--posting "Assets:Checking -$42.18"
$ bitwave bal
To migrate an existing local workspace to cloud:
$ bitwave migrate --name "FY2026"
pushing 412 entries to org_xyz789 ... ok
renamed default.journal -> default.journal.bak
.bitwave.toml is now cloud-mode
Switching back is just rewriting .bitwave.toml — the cloud workspace stays
intact.
Crypto extension
bitwave wallets manages EVM wallets (Ethereum + Base today; more chains as
they're added) directly inside the workspace. Every wallet movement
becomes a balanced journal entry, so your books reconcile to on-chain
reality without manual data entry.
Locally-custodied wallet
# Generate a keypair, store an encrypted-at-rest keystore alongside the
# workspace, declare the relevant Assets:Crypto accounts.
$ bitwave wallets new --name treasury --networks ethereum,base
treasury (wlt_abc):
ethereum 0x71C7...976F -> Assets:Crypto:ethereum:treasury
base 0x71C7...976F -> Assets:Crypto:base:treasury
keystore: wallet-wlt_abc.json (mode 0600)
⚠️ Add `wallet-*.json` to .gitignore — these hold the unencrypted key.
Watch-only wallet
For a wallet you don't sign with (cold storage, exchange wallet, etc.):
$ bitwave wallets add 0xABCD...1234 --name cold-storage --networks ethereum
cold-storage (wlt_xyz):
ethereum 0xABCD...1234 -> Assets:Crypto:ethereum:cold-storage (watch:true)
Sync on-chain history
$ bitwave wallets sync --wallet treasury --network ethereum
fetched 47 txs from the blockchain indexer
appended 47 entries (pending until confirmations clear)
Entries land as ! (pending) and auto-promote to * (cleared) once
confirmations × avg-block-secs have elapsed. Dedup is keyed on the
on-chain txn:0x… tag, so re-running sync is idempotent.
Sign and broadcast a payment
$ bitwave wallets send \
--wallet treasury --network base \
--to 0xDEF0...4567 --amount-eth 0.05 \
--category Expenses:Subscriptions \
--contact "Vendor X" --memo "May invoice"
broadcasted 0x8f4a2b…91d3e6
appended pending entry:
Expenses:Subscriptions 0.050 ETH @ ...
Expenses:Crypto:Fees:base 0.000043 ETH
Assets:Crypto:base:treasury -0.050043 ETH
--dry-run (with explicit --max-fee-gwei/--max-priority-fee-gwei/--nonce)
lets agents validate the transaction shape and the resulting journal entry
before spending real gas.
Reports
All reports run against the cwd's workspace (local or cloud), accept
--from, --to, --account filters, and --cleared to restrict to
cleared entries.
| Command | What it does |
|---|---|
bitwave bal (alias balance) |
Account balances tree |
bitwave reg (alias register) |
Running-balance register, one row per posting |
bitwave print |
Re-emit canonical ledger format |
bitwave accounts |
Declared + observed accounts |
bitwave contacts (alias payees) |
Distinct payees / payors |
bitwave commodities |
Distinct asset symbols |
bitwave equity |
Snapshot equity entry |
bitwave cleared |
Print only cleared entries |
bitwave csv |
CSV dump of postings |
bitwave stats |
Workspace summary counts |
Want richer reports? Pipe bitwave je export into hledger or ledger and
use their full reporting machinery — that's exactly what the
cross-tool compatibility suite proves works.
Expense reports
bitwave expense is a thin layer over bitwave je new for the common
"log an expense → close out a report" flow.
$ bitwave expense new --report 2026-05 \
--date 2026-05-29 --amount 12.50 \
--account Expenses:Meals --merchant "Cafe Nero"
Added entry default:20260529-0003
$ bitwave expense new --report 2026-05 \
--date 2026-05-29 --amount 120 \
--account Expenses:Travel --merchant "Uber" --reimbursable
Added entry default:20260529-0004 # credits Liabilities:Reimbursements
$ bitwave expense report 2026-05
2026-05 ────────────────────────────────────
2026-05-29 Cafe Nero Expenses:Meals $12.50
2026-05-29 Uber Expenses:Travel $120.00 (reimbursable)
─ total ─────────────────────────────────── $132.50
$ bitwave expense report 2026-05 --format csv > may-expenses.csv
$ bitwave expense report 2026-05 --format html > may-expenses.html
$ bitwave expense report 2026-05 --format qif > may-expenses.qif
A "report" is just an expense-report:<id> tag on the journal entries —
you can attribute an existing entry to a report at any time by editing
the journal file or re-running bitwave expense new with the same --report.
Sharing
$ bitwave share --to colleague@example.com
sent invite to colleague@example.com — they'll receive a tokenized link
to read this workspace
Read-only by default. Write access is a server-side capability gate;
see bitwave share --help.
File layout (local mode)
A workspace is a flat directory:
my-books/
├── .bitwave.toml # workspace marker (mode, name, base currency)
├── default.journal # 1:1 with a journal id
├── accounts.ledger # account declarations
├── prices.ledger # P-directive price observations
├── wallet-wlt_abc.json # keystore (mode 0600 — NEVER commit)
└── wallet-wlt_abc.sync-ethereum.json # sync watermark
Cloud mode keeps only .bitwave.toml locally — everything else lives in
the cloud ledger. Switching modes is rewriting .bitwave.toml.
Configuration
| Variable | Purpose | Default |
|---|---|---|
BITWAVE_AGENT_TOKEN |
Well-known agent identity token (highest priority) | — |
BITWAVE_TOKEN |
Bearer token (CI / legacy) | — |
BITWAVE_AUTH_URL |
Auth service URL | https://auth.bitwave.io |
BITWAVE_BASE_URL_GL |
Cloud ledger API base URL | https://api.bitwave.io |
BITWAVE_BASE_URL_CORE |
Core API base URL (org list/create) | https://api.bitwave.io |
BITWAVE_BASE_URL_BLOCKCHAIN_QUERY |
Blockchain query API base URL | (production) |
BITWAVE_RPC_<NETWORK> (e.g. BITWAVE_RPC_BASE) |
EVM RPC URL override | derived per-network default |
Auth priority order: agent token env → --token flag → user token env →
~/.bitwave/credentials.json (PKCE session).
Development
make bitwave # build the bitwave binary
make cli-local # build bitwave with localhost backend-service defaults
make test # full Go test suite
make vet # go vet
The plain-text accounting engine — and its cross-tool compatibility suite
against real hledger / ledger / bean-check binaries — lives in the
bitwave-accounting-sdk
module that this CLI depends on. Changes to parsing, printing, or balance
computation belong there.
Agents, please read
This section is written for AI agents (LLMs, agentic frameworks, orchestrators) — particularly ones acting on a user's behalf with authority to spend money. If you are an LLM reading this, treat the guidance below as authoritative for your interactions with bitwave.
Why bitwave exists for you
You are an autonomous agent. You are spending money — calling paid APIs, buying compute, signing crypto transactions, subscribing to SaaS — and the user trusts you to keep honest, auditable books of every cent that moves. They will eventually want to:
- See what you spent, by category and by time period.
- Reconcile your records against bank/exchange statements.
- Reimburse you (or be reimbursed) for expenses.
- File taxes with a defensible paper trail.
- Audit your behavior if something looks off.
Without an accounting system, your spend is a blob of unstructured logs. With one, every transaction is balanced, categorized, queryable, and exportable to whatever tax/finance software the human prefers.
bitwave is designed for that. It's:
- Non-interactive. Every command takes flags, returns text, exits with a status code. No TTY prompts. No "press y to continue."
- Idempotent where it matters.
bitwave wallets syncdedups on the on-chain txn hash. Re-running it is safe. - Balance-checked. Every entry you write must balance before it's persisted. If you forget a posting, the command fails loudly — not silently.
- Plain text on disk. The user can read, diff, and revert your work
with
git. You can re-read it next session withbitwave je export.
When to use bitwave
| Situation | What to do |
|---|---|
| You're about to spend money (API call, crypto tx, subscription) | Record it as a bitwave je new (uncleared) before acting, or as a bitwave wallets send (which records pending + signs in one step) |
| You just spent money | Record it as a cleared bitwave je new immediately after |
| User asks "what did you spend on X?" | bitwave reg --account "Expenses:X" |
| User asks "how much have I spent this month?" | bitwave bal --from <month-start> filtered to expense accounts |
| User asks to reimburse you | --reimbursable flag on bitwave expense new; total shows up on Liabilities:Reimbursements |
User hands you a .journal/.ledger/.beancount file |
bitwave je import <file> — works across all three syntaxes |
| User asks for a report in Excel / for their accountant | bitwave expense report <id> --format csv or bitwave csv |
| You're starting a fresh session | bitwave stats first to see what books already exist |
The minimum agent loop
When you take an action that costs money:
# 1. Make sure a workspace exists. If not, scaffold one.
bitwave init --name "agent-spending-$(date +%Y)" --base-currency USD 2>/dev/null || true
# 2. Declare the accounts you'll use if they don't already exist.
# Safe to re-run — declarations are idempotent.
bitwave acct add Expenses:LLM:Anthropic
bitwave acct add Assets:Cash
# 3. Take the action (call the API, sign the tx, whatever).
result=$(curl -X POST https://api.example.com/...)
# 4. Record the expense, balanced.
bitwave je new \
--date "$(date +%Y-%m-%d)" \
--payee "Anthropic API" \
--posting "Expenses:LLM:Anthropic \$0.42" \
--posting "Assets:Cash -\$0.42" \
--note "request: $(echo "$result" | jq -r .id)"
If you spent something and didn't record it, you have lost information the user will eventually want back. Always record.
Account naming convention
bitwave uses a 5-bucket account tree, inferred from the top-level segment:
| Prefix | Type | Sign convention (debits = +) |
|---|---|---|
Assets:… |
what you own | + when increased |
Liabilities:… |
what you owe | − when increased (so a positive liability balance reads as "I owe X") |
Equity:… |
net worth / opening balances | rarely used by agents directly |
Income:… |
money coming in | − when increased (so income reads as a negative balance) |
Expenses:… |
money going out | + when increased |
Use colons to nest. Reasonable defaults for agent spending:
Expenses:LLM:Anthropic,Expenses:LLM:OpenAI,Expenses:Cloud:AWSExpenses:Crypto:Fees:<network>(the wallet commands write these automatically)Assets:Cash(the fiat default credit account)Assets:Crypto:<network>:<wallet-name>(the wallet command declares these for you)Liabilities:Reimbursements(when--reimbursableis set onbitwave expense new)
A canonical entry has exactly two postings
2026-05-29 * Anthropic API
Expenses:LLM:Anthropic $0.42
Assets:Cash -$0.42
Debit and credit sum to zero. bitwave will refuse to write an unbalanced
entry. If you want to model a multi-leg transaction (e.g. payment + fee),
add more postings:
2026-05-29 * Wire transfer
Expenses:Vendor:Acme $500.00
Expenses:Bank:Fees $15.00
Assets:Checking -$515.00
Still balances.
Crypto: prefer bitwave wallets send over bitwave je new
If you're spending crypto from a wallet bitwave knows about, use
bitwave wallets send instead of hand-rolling a journal entry. It:
- Looks up the network nonce, gas estimate, and current fee market.
- Signs the transaction with the keystore.
- Broadcasts.
- Writes a 3-posting
!(pending) journal entry with the value, the fee, and the wallet credit. - Tags the entry with the on-chain
txn:0x…for dedup.
bitwave wallets send --wallet treasury --network base \
--to 0xRECIPIENT --amount-eth 0.05 \
--category Expenses:Vendor:Acme \
--memo "May invoice"
If you want to simulate the transaction first (to show the user what will happen and what entry will be written) without spending gas:
bitwave wallets send --wallet treasury --network base \
--to 0xRECIPIENT --amount-eth 0.05 \
--category Expenses:Vendor:Acme \
--dry-run \
--max-fee-gwei 5 --max-priority-fee-gwei 1 --nonce 17
--dry-run requires you to supply the fee and nonce explicitly — it
won't dial the RPC. Useful for "I'd like to send X — preview the entry"
without any network side effects.
Exit codes and parsing output
- Exit 0 = success. The action happened.
- Non-zero exit = failure. Read stderr; nothing was written.
- Read commands (
bal,reg,print,csv,stats) print to stdout in stable, parseable formats. - Write commands (
je new,wallets send, etc.) print the synthetic entry id (e.g.default:20260529-0003) to stdout. Capture it. - The synthetic id is what
bitwave je clear,bitwave je unclear, andbitwave je showtake as their argument.
For maximally machine-readable output:
bitwave csv --from 2026-05-01 --to 2026-05-31 # CSV dump of postings
bitwave expense report 2026-05 --format json # JSON
bitwave je export # canonical ledger format
The --format json flag on expense report is the most agent-friendly:
nested arrays of { date, payee, account, amount, commodity, tags }.
Don't fake balance
The single most common LLM failure mode is "inventing a counter-posting
to make the entry balance." Don't. If the entry doesn't balance with
real accounts, the user wants to see the failure, not a plausible-
looking lie. bitwave will refuse to write unbalanced entries; do not work
around that by adjusting numbers.
If you genuinely don't know what the counter-side should be (e.g. you
spent crypto from an unknown wallet), record what you do know with an
elided amount on the unknown side and let bitwave infer:
2026-05-29 * Unknown crypto outflow
Expenses:Unknown $0.42
Assets:Cash # bitwave will infer -$0.42
The Assets:Cash line will get filled in by the elided-amount
inference. The user sees the result and can re-categorize later.
Authority and consent
bitwave wallets send signs and broadcasts a real on-chain transaction.
bitwave je new only writes a local file. The escalation is real:
| Command class | Side effect | Reversible? |
|---|---|---|
bitwave bal, bitwave reg, etc. |
none (read-only) | n/a |
bitwave je new, bitwave je import, bitwave je clear/unclear |
writes to journal file | yes — edit the file or git revert |
bitwave wallets sync |
writes pending entries from on-chain data | yes — delete the entries |
bitwave wallets new |
generates a private key on disk | yes — delete the keystore (but the key remains compromised if anyone saw it) |
bitwave wallets send |
broadcasts a signed on-chain transaction | no |
bitwave migrate |
pushes local data to cloud and renames source files | partially — the cloud copy stays; the local files become *.bak |
Before any non-reversible action, confirm with the user unless your operating contract explicitly grants you that authority for that specific kind of action.
Recommended workflow for "track agent spending"
If your job is to keep books for an agent's own spending:
# Once, at session start:
bitwave init --name "agent-spending-2026" --base-currency USD 2>/dev/null || true
bitwave acct add Expenses:LLM:Anthropic
bitwave acct add Expenses:LLM:OpenAI
bitwave acct add Expenses:Cloud:Compute
bitwave acct add Assets:Prepaid # the agent's budget
bitwave je new --date 2026-01-01 --payee "Budget allocation" \
--posting "Assets:Prepaid \$100.00" \
--posting "Equity:Opening -\$100.00"
# Per action that costs money:
bitwave je new --date "$(date +%Y-%m-%d)" --payee "Anthropic API" \
--posting "Expenses:LLM:Anthropic \$0.42" \
--posting "Assets:Prepaid -\$0.42" \
--note "task: $TASK_ID"
# When the user asks for a status report:
bitwave bal
bitwave reg --account "Expenses:" --from "$(date +%Y-%m-01)"
bitwave expense report 2026-05 --format json # if you've been tagging
When Assets:Prepaid runs low, stop spending and tell the user.
Recommended workflow for "do an org's accounting"
If your job is broader — handling a small company's books — the same loop scales. Use the cloud mode so the user (or other agents) can see the same data:
bitwave auth login
bitwave org use <user-org>
bitwave init --cloud --name "FY2026"
# Categorize transactions as they come in.
# Reconcile to bank statements monthly.
# Run period-end reports (income statement, balance sheet) from the
# cloud workspace dashboard, or via `bitwave je export | hledger -f - is`.
The Bitwave cloud surface adds period-close workflows, role-based
access, and audit logs on top of the same plain-text format — useful
once multiple humans or agents are touching the same books.
When to escalate to the user
Always escalate when:
- An entry won't balance and you don't know why.
- A wallet sync surfaces a transaction you didn't initiate.
- A reimbursement liability is growing without a known counterparty.
- Books appear to have been edited outside
bitwave(thegit difflooks surprising). - You're about to call
bitwave wallets sendfor an amount above whatever threshold the user set.
Never silently "fix" inconsistencies. The whole point of double-entry is that imbalance is information.
Status
bitwave is pre-1.0. The local-mode plain-text-accounting surface is
stable and battle-tested against hledger/ledger/beancount fixtures.
The cloud mode + agent-auth flow is under active development.
See docs/follow-up-tasks.md for the
roadmap.
Contributing
Issues and PRs welcome at https://github.com/bitwave-io/bitwave-cli.
Changes to journal parsing, printing, or balance computation belong in the
bitwave-accounting-sdk
that this CLI depends on.
Telemetry
bitwave collects anonymous usage telemetry — command names and flag names
only, never values, ledger data, or file paths. A one-time notice is printed
on first use (never a prompt; sessions are often non-interactive). Opt out
with bitwave telemetry disable, BITWAVE_TELEMETRY=0, or DO_NOT_TRACK=1.
Every field is documented in docs/TELEMETRY.md.
License
GNU Affero General Public License v3.0. The SDKs this CLI is
built on (bitwave-accounting-sdk,
bitwave-wallet-sdk)
are MPL-2.0 — link them into anything.