hanzo-ledger

Hanzo Ledger is a programmable double-entry financial ledger written in Go.

Hanzo Ledger - Programmable Double-Entry Financial Ledger

Category: Hanzo Ecosystem Related Skills: hanzo/hanzo-vault.md, hanzo/hanzo-commerce-api.md, hanzo/hanzo-commerce.md

Overview

Hanzo Ledger is a programmable double-entry financial ledger written in Go. It provides atomic multi-posting transactions, account-based modeling, and a scriptable DSL called Numscript for expressing complex money movements. Fork of Formance Ledger (Go module still github.com/formancehq/ledger).

Why Hanzo Ledger?

Architecture

hanzo/commerce Storefront, catalog, orders
 |
hanzo/payments Payment routing (50+ processors)
 |
hanzo/treasury Ledger, reconciliation, wallets <-- this
 |
lux/treasury On-chain treasury, MPC/KMS wallets

When to use

Hard requirements

  1. PostgreSQL (16+ recommended) for transaction storage
  2. Go 1.24+ for building from source
  3. Docker for local development (compose includes Postgres, Prometheus, Jaeger, OTEL collector)

Quick reference

| Item | Value | |------|-------| | Language | Go | | Go Module | github.com/formancehq/ledger | | API | HTTP REST (chi router) | | Port | 3068 | | Database | PostgreSQL (via uptrace/bun ORM) | | API Versions | v1 (legacy), v2 (current), v3 (OpenAPI spec exists) | | CLI Commands | serve, worker, buckets upgrade, version, docs | | Event Bus | Watermill (NATS, Kafka, HTTP) | | Observability | OpenTelemetry (traces + metrics), Prometheus, Jaeger | | Repo | github.com/hanzoai/ledger | | License | MIT |

API Reference

Create a Transaction

POST /v2/ledger/{name}/transactions
Content-Type: application/json

{
 "postings": [{
 "source": "world",
 "destination": "users:001",
 "amount": 10000,
 "asset": "USD/2"
 }]
}

Create Transaction with Numscript

POST /v2/ledger/{name}/transactions
Content-Type: application/json

{
 "script": {
 "plain": "send [USD/2 10000] (\n source = @users:001\n destination = {\n 90% to @merchants:042\n 10% to @platform:fees\n }\n)"
 }
}

List Transactions

GET /v2/ledger/{name}/transactions

Get Account Balance

GET /v2/ledger/{name}/accounts/{address}

Set Account Metadata

POST /v2/ledger/{name}/accounts/{address}/metadata
Content-Type: application/json

{
 "role": "merchant",
 "tier": "gold"
}

Revert a Transaction

POST /v2/ledger/{name}/transactions/{id}/revert

Bulk Operations

POST /v2/ledger/{name}/bulk
Content-Type: application/json

[
 {"action": "CREATE_TRANSACTION", "data": {"postings": [...]}},
 {"action": "ADD_METADATA", "data": {"targetType": "ACCOUNT", "targetID": "users:001", "metadata": {...}}}
]

Health and Info

GET /_healthcheck
GET /_/info
GET /_/metrics

Numscript DSL

Numscript is a purpose-built language for expressing money movements.

// Simple transfer
send [USD/2 10000] (
 source = @users:001
 destination = @merchants:042
)

// Multi-party fee split
send [USD/2 10000] (
 source = @users:001
 destination = {
 90% to @merchants:042
 10% to {
 50% to @platform:fees
 50% to @platform:reserve
 }
 }
)

Core Concepts

Accounts

Hierarchical addresses separated by colons: users:001, merchants:042:fees. The special account world is an unlimited source.

Postings

Each posting moves an amount of an asset from source to destination:

type Posting struct {
 Source string // source account address
 Destination string // destination account address
 Amount *big.Int // amount (arbitrary precision)
 Asset string // asset identifier (e.g., "USD/2")
}

Volumes

Every account tracks input/output volumes per asset. Balance = Input - Output.

Ledgers and Buckets

Multiple ledgers can share a single database (bucket). Each ledger has independent accounts, transactions, and metadata. Bucket names and ledger names must match ^[0-9a-zA-Z_-]{1,63}$.

Chart of Accounts

Optional schema enforcement. Define allowed account structures with fixed segments, variable segments ($variable), and regex patterns.

Features

Per-ledger configurable features:

| Feature | Options | Default | |---------|---------|---------| | MOVES_HISTORY | ON, OFF | ON | | MOVES_HISTORY_POST_COMMIT_EFFECTIVE_VOLUMES | SYNC, DISABLED | SYNC | | HASH_LOGS | SYNC, ASYNC, DISABLED | SYNC | | ACCOUNT_METADATA_HISTORY | SYNC, DISABLED | SYNC | | TRANSACTION_METADATA_HISTORY | SYNC, DISABLED | SYNC |

Development

Build and Test

go build ./...
go test ./...

# With integration tests
go test -tags it ./...

# Full test with coverage
just tests

Local Development with Docker

docker compose up -d
# Ledger available at http://localhost:3068

Docker compose includes: PostgreSQL 16, Prometheus, Jaeger, OTEL collector, ledger server, and worker.

Task Runner (Justfile)

just # list available commands
just lint # golangci-lint
just tidy # go mod tidy
just generate # go generate
just tests # full test suite with coverage
just openapi # regenerate OpenAPI spec
just generate-client # regenerate Go client SDK (requires SPEAKEASY_API_KEY)
just pre-commit # tidy + generate + lint + openapi (run before commits)

Project Structure

ledger/
 cmd/ CLI commands (serve, worker, buckets, docs, version)
 internal/
 api/ HTTP API (v1, v2 routers, bulking)
 bus/ Event publishing (Watermill)
 controller/ Business logic (ledger + system controllers)
 machine/ Numscript execution engine
 queries/ Query building
 replication/ Data export pipelines (gRPC, drivers)
 storage/ PostgreSQL storage layer (bun ORM)
 tracing/ OpenTelemetry instrumentation
 pkg/
 accounts/ Account address validation
 assets/ Asset format validation
 client/ Generated Go client SDK (Speakeasy)
 events/ Event type definitions
 features/ Feature flag definitions
 openapi/ OpenAPI specs (v1, v2, v3)
 deployments/ Docker Compose + Pulumi configs
 examples/ Example setups (standalone, publisher-http, publisher-kafka)
 docs/ Generated documentation
 tools/ Build tooling

Configuration

Environment Variables

| Variable | Description | Default | |----------|-------------|---------| | POSTGRES_URI | PostgreSQL connection string | required | | POSTGRES_MAX_OPEN_CONNS | Max open connections | -- | | POSTGRES_MAX_IDLE_CONNS | Max idle connections | -- | | POSTGRES_CONN_MAX_IDLE_TIME | Connection max idle time | -- | | AUTO_UPGRADE | Auto-upgrade schemas on start | false | | EXPERIMENTAL_FEATURES | Enable feature configurability | false | | BULK_PARALLEL | Bulk operation parallelism | 10 | | DEBUG | Enable debug mode | false |

CLI Flags (serve command)

| Flag | Description | Default | |------|-------------|---------| | --bind | API bind address | 0.0.0.0:3068 | | --auto-upgrade | Auto-upgrade schemas | false | | --numscript-cache-max-count | Numscript cache size | 1024 | | --bulk-max-size | Max bulk operation size | 100 | | --bulk-parallel | Bulk parallelism | 10 | | --default-page-size | Default pagination size | 15 | | --max-page-size | Max pagination size | 100 | | --worker | Enable embedded worker | false | | --schema-enforcement-mode | Schema enforcement (audit, strict) | audit |

Key Dependencies

| Package | Purpose | |---------|---------| | github.com/formancehq/numscript | Numscript DSL parser/interpreter | | github.com/formancehq/go-libs/v4 | Shared library (auth, ORM, health, OTLP, publishing) | | github.com/go-chi/chi/v5 | HTTP router | | github.com/uptrace/bun | PostgreSQL ORM | | github.com/jackc/pgx/v5 | PostgreSQL driver | | github.com/ThreeDotsLabs/watermill | Event bus (NATS, Kafka, HTTP) | | github.com/spf13/cobra | CLI framework | | go.uber.org/fx | Dependency injection | | go.opentelemetry.io/otel | Observability | | github.com/antlr/antlr4 | ANTLR parser runtime (Numscript grammar) |

Troubleshooting

| Issue | Cause | Solution | |-------|-------|----------| | Connection refused on 3068 | Server not running | docker compose up -d or go run main.go serve | | Schema not found | Ledger not created or not upgraded | Set AUTO_UPGRADE=true or run ledger buckets upgrade | | Insufficient funds | Source account balance too low | Use @world as source or fund the account first | | Invalid posting | Negative amount or invalid account address | Amounts must be >= 0, addresses match ^[a-zA-Z0-9_:]+$ | | Duplicate reference | Transaction reference already used | Use a unique reference field or omit it | | Numscript parse error | Invalid Numscript syntax | Check DSL syntax, ensure assets use ASSET/precision format |

Related Skills


Last Updated: 2026-03-13 Category: Hanzo Ecosystem Related: ledger, accounting, double-entry, numscript, financial, transactions, go Prerequisites: Double-entry accounting concepts, Go, PostgreSQL, Docker