Hanzo Numscript
Numscript is a domain-specific language for modeling complex financial transactions. It provides declarative syntax for multi-party fund transfers with percentage splits, ordered funding sources, o...
Overview
Numscript is a domain-specific language for modeling complex financial transactions. It provides declarative syntax for multi-party fund transfers with percentage splits, ordered funding sources, overdraft controls, and parameterized scripts. Used by Hanzo Ledger for programmable money movement. Includes a CLI, Go library, LSP server, and MCP server.
Why Numscript?
- Declarative transfers -- Express multi-party fund routing in readable syntax
- Percentage splits -- Route funds by percentage or fixed amount to multiple destinations
- Source ordering -- Define fallback funding sources with automatic overdraft prevention
- Type safety -- Compile-time validation of account references and asset types
- ANTLR grammar -- Formal grammar (Numscript.g4 + Lexer.g4) for parsing
- LSP support -- Language Server Protocol for editor integration
- MCP server -- Model Context Protocol integration for AI tooling
Tech Stack
- Language: Go 1.24
- Parser: ANTLR4 (Go target)
- CLI: cobra (goreleaser for cross-platform builds)
- Testing: testify + go-snaps (snapshot testing)
- Error tracking: Sentry
- LSP: go.lsp.dev/protocol
- MCP: mark3labs/mcp-go
- License: MIT
OSS Base
Repo: hanzoai/numscript (Formance numscript fork). Go module path is github.com/formancehq/numscript (upstream module name retained).
When to use
- Modeling multi-party financial transfers (e.g., marketplace payouts)
- Splitting payments by percentage across multiple destinations
- Defining ordered funding sources with overdraft limits
- Parameterizing transaction scripts for reuse
- Validating transaction scripts before execution (parse + analysis)
- Integrating financial transaction DSL into a ledger system
Hard requirements
- Go 1.24+ for building from source
- ANTLR4 for regenerating the parser from grammar files
- Hanzo Ledger for executing scripts against real accounts (optional for parsing)
Quick reference
| Item | Value |
|---|---|
| Go module | github.com/formancehq/numscript |
| CLI binary | numscript |
| Go version | 1.24 |
| Grammar | Numscript.g4 + Lexer.g4 (ANTLR4) |
| Default branch | main |
| License | MIT |
| Repo | github.com/hanzoai/numscript |
One-file quickstart
Install CLI
go install github.com/hanzoai/numscript/cmd/numscript@latestParse and validate
numscript check script.num
numscript fmt script.numGo library
package main
import (
"context"
"fmt"
"math/big"
"github.com/formancehq/numscript"
)
func main() {
program := numscript.Parse(`
send [USD/2 5000] (
source = @users:alice
destination = {
85% to @merchants:shop
10% to @platform:fees
5% to @platform:reserve
}
)
`)
if errs := program.GetParsingErrors(); len(errs) > 0 {
fmt.Println("Parse errors:", numscript.ParseErrorsToString(errs))
return
}
// Get needed variables (for parameterized scripts)
vars := program.GetNeededVariables()
fmt.Println("Variables:", vars)
// Execute against a store
result, err := program.Run(context.Background(),
numscript.VariablesMap{},
&numscript.StaticStore{
Balances: numscript.Balances{
"users:alice": {"USD/2": big.NewInt(10000)},
},
},
)
if err != nil {
fmt.Println("Error:", err)
return
}
for _, posting := range result.Postings {
fmt.Printf("%s -> %s: %s %s\n",
posting.Source, posting.Destination,
posting.Amount, posting.Asset)
}
}Core Concepts
Language Syntax
// Variables with types
vars {
monetary $amount
account $sender
account $receiver
}
// Simple transfer
send $amount (
source = $sender
destination = $receiver
)
// Multi-source with fallback (ordered, first with funds wins)
send [USD/2 10000] (
source = {
@users:alice
@users:alice:savings
@world // infinite source (minting)
}
destination = @merchants:shop
)
// Percentage split destination
send [USD/2 10000] (
source = @users:alice
destination = {
85% to @merchants:shop
10% to @platform:fees
remaining to @platform:reserve
}
)
// Overdraft controls
send [USD/2 100] (
source = {
@a allowing overdraft up to [USD/2 10]
@b allowing unbounded overdraft
}
destination = @dest
)
// Save (earmark funds)
save [USD/2 10] from @alice
// Metadata-driven accounts
vars {
account $dest = meta(@config, "payout_account")
}
// Function calls
set_tx_meta("key", "value")Architecture
Parse Run
numscript.go ──────────► ParseResult ──────► ExecutionResult
| |
| ANTLR4 parser | interpreter
| (Lexer.g4 + | (balance queries,
| Numscript.g4) | metadata queries,
| | posting generation)
| |
v v
ParserError[] Posting[]
Metadata{}
AccountsMetadata{}Public API
// Parse source code
numscript.Parse(code string) ParseResult
// Inspect parsed result
ParseResult.GetParsingErrors() []ParserError
ParseResult.GetNeededVariables() map[string]string
ParseResult.GetSource() string
// Execute
ParseResult.Run(ctx, vars, store) (ExecutionResult, InterpreterError)
ParseResult.RunWithFeatureFlags(ctx, vars, store, flags) (ExecutionResult, InterpreterError)
// Types
type Posting struct {
Source, Destination string
Amount *big.Int
Asset string
}
type ExecutionResult struct {
Postings []Posting
Metadata Metadata // set_tx_meta() results
AccountsMetadata AccountsMetadata // set_account_meta() results
}
// Store interface (implement for your ledger)
type Store interface {
GetBalances(ctx, BalanceQuery) (Balances, error)
GetAccountsMetadata(ctx, MetadataQuery) (AccountsMetadata, error)
}Feature Flags
flags.ExperimentalOneofFeatureFlag // oneof {} source selection
flags.ExperimentalMidScriptFunctionCall // balance() calls mid-scriptDirectory structure
github.com/hanzoai/numscript/
numscript.go # Public API: Parse, Run, types
numscript_test.go # Integration tests (20+ test cases)
Numscript.g4 # ANTLR4 grammar (parser rules)
Lexer.g4 # ANTLR4 grammar (lexer rules)
Justfile # Build commands (generate, test, lint, release)
.goreleaser.yaml # Cross-platform binary releases
inputs.schema.json # JSON schema for script inputs
specs.schema.json # JSON schema for specs
cmd/numscript/
main.go # CLI entrypoint (check, fmt subcommands)
internal/
parser/ # ANTLR4-generated parser + parse tree
interpreter/ # Script execution engine
analysis/ # Static analysis and validation
lsp/ # Language Server Protocol implementation
mcp_impl/ # Model Context Protocol server
jsonrpc2/ # JSON-RPC 2.0 transport
cmd/ # CLI command implementations
flags/ # Feature flag definitions
ansi/ # Terminal color output
specs_format/ # Spec formatting utilities
utils/ # Shared utilitiesTroubleshooting
| Issue | Cause | Solution |
|---|---|---|
Parse error on @world | Missing source block | @world must be inside a source = {} block |
| MissingFundsErr | Source accounts have insufficient balance | Add @world as final fallback or use allowing overdraft |
| Variables not resolved | Missing from VariablesMap | Check GetNeededVariables() and provide all required vars |
| ANTLR regeneration fails | ANTLR4 not installed | Install via brew install antlr or use Nix flake |
| Module path mismatch | Upstream module name | Import as github.com/formancehq/numscript (retained from fork) |
Related Skills
hanzo/hanzo-reconciliation.md- Transaction reconciliation enginehanzo/hanzo-billing.md- Billing and subscription management
How is this guide?
Last updated on
Hanzo Ledger - Programmable Double-Entry Financial Ledger
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 expressi...
Hanzo Payments - Payment Orchestration Switch
Hanzo Payments is an open-source payment orchestration switch written in Rust. It routes payments across 50+ processors with smart retries, fallback cascading, and unified analytics. Based on Hyper...