Parse OFX/QFX bank downloads and CSV bank exports into one normalized transaction schema. A small Go library and a single static CLI binary, with no runtime dependencies.
Banks hand you two very different things: an OFX or QFX download (SGML or XML),
or a CSV with whatever column names that bank happened to choose. This tool
reads both and emits the same clean JSON every time: signed decimal amounts, a
consistent sign convention, dates as YYYY-MM-DD, and an explicit debit/credit
direction. Drop it into an import pipeline and the rest of your code only ever
sees one shape.
- OFX / QFX - reads both export styles in one tokenizer: OFX 1.x SGML (the
unclosed
<TRNAMT>-42.00tag style with a colon-delimited header) and OFX 2.x XML. Pulls account id, type, routing number, and currency from the statement, and per-transaction date, amount, type, memo, check number, and theFITIDstable id. - CSV - handles both the single signed-amount layout and the split debit/credit layout, with fuzzy header detection across a broad alias table (Date / Posting Date / Transaction Date, Description / Memo / Payee, and so on), so the common exports from major banks parse without per-bank config.
- One schema - every parse yields the same
Statementshape with a stable schema version, so downstream code is written once. - Money-safe - amounts stay decimal strings, never binary floats. It understands currency symbols, thousands separators, accounting parentheses for negatives, and trailing-minus signs.
Build the CLI from source (Go 1.22 or newer):
go build -o ofxnorm ./cmd/ofxnormOr install it onto your PATH:
go install github.com/maxed-oss/ofx-normalizer/cmd/ofxnorm@latestUse it as a library:
go get github.com/maxed-oss/ofx-normalizer# Auto-detect format from the file extension and content.
ofxnorm statement.ofx
ofxnorm export.csv
# Read from stdin and force a format.
cat download.qfx | ofxnorm --format ofx
# Some banks export spending as a positive number; flip the sign convention.
ofxnorm --format csv --invert-amounts export.csv
# Compact output.
ofxnorm --pretty=false statement.ofxFlags:
| Flag | Default | Meaning |
|---|---|---|
--format |
auto |
auto, ofx, or csv |
--pretty |
true |
pretty-print the JSON |
--invert-amounts |
false |
flip the sign of the CSV signed-amount column |
--json-errors |
false |
on failure, print a JSON error envelope to stderr |
--version |
print version and exit |
Exit codes are stable: 0 on success, 1 on a parse or input error, 2 on a
flag/usage error. With --json-errors, a failure prints
{"ok": false, "error": {"type": "parse_error", "message": ...}} to stderr.
The tool speaks one format, version 1.0:
{
"schema_version": "1.0",
"source_format": "ofx",
"account": {
"id": "0000123456789",
"type": "CHECKING",
"routing_number": "121000248"
},
"transactions": [
{
"date": "2024-01-05",
"amount": "-42.00",
"currency": "USD",
"direction": "debit",
"description": "COFFEE SHOP - CARD PURCHASE",
"type": "DEBIT",
"id": "202401050001"
}
]
}Field notes:
amountis a signed decimal string. Money leaving the account is negative (a debit); money entering is positive (a credit). This matches the OFXTRNAMTconvention and is applied uniformly to CSV input too.directionis"debit"or"credit", derived from the sign ofamountand provided explicitly so consumers do not re-derive it.dateis alwaysYYYY-MM-DD.idis the source stable id when present (the OFXFITID), which makes a strong dedup key.- Optional fields (
currency,type,check_number, account metadata) are emitted only when the source provides them.
package main
import (
"fmt"
"github.com/maxed-oss/ofx-normalizer/pkg/csvbank"
"github.com/maxed-oss/ofx-normalizer/pkg/normalize"
"github.com/maxed-oss/ofx-normalizer/pkg/ofx"
)
func main() {
stmt, err := ofx.Parse(ofxContent)
if err != nil {
panic(err)
}
fmt.Println(len(stmt.Transactions), "transactions")
// CSV with options.
csvStmt, err := csvbank.Parse(csvContent, csvbank.Options{InvertAmounts: true})
if err != nil {
panic(err)
}
// Detect the format when you do not know it up front.
switch normalize.Detect(content, "download.qfx") {
case normalize.FormatOFX:
// ...
case normalize.FormatCSV:
// ...
}
}Packages:
pkg/normalize- the sharedStatementandTransactionschema, plus the amount, date, and format-detection helpers.pkg/ofx- the OFX/QFX parser.pkg/csvbank- the CSV parser.
- OFX / QFX - Open Financial Exchange, the long-standing bank-download format, in both the 1.x SGML and 2.x XML serializations.
- Output - the normalized JSON schema documented above, version
1.0.
Given the same input, ofx-normalizer produces the same output, byte for byte. Parsing does not depend on machine locale, and amounts are kept as decimal strings, so the data round-trips cleanly into any bookkeeping or reconciliation step that consumes it.
go vet ./...
go test ./...Tests are table-driven and run against the fixture files in testdata/.
For AI agents and automation: pipe a statement to ofxnorm - and read
normalized JSON on stdout; add --json-errors for a structured error envelope
on stderr and branch on the stable exit codes above. The full machine interface
is in AGENT.md and llms.txt. This binary backs the
normalize_ofx tool in maxed-mcp.
MIT. See LICENSE.