Skip to main content

Turn messy bank/card transaction descriptors into clean structured data — US and India, agentically.

Project description

matlas

Turn messy bank-statement strings into clean, structured data.

If you've ever pulled transactions from a bank feed, you know the problem: what a human calls "coffee at Starbucks" arrives as SQ *STARBUCKS #4521 SEATTLE WA, and a food delivery order in India shows up as UPI/DR/408123456789/SWIGGY/YESB/Payment for order. Closed APIs like Plaid Enrich, Ntropy, and MX solve this behind a paywall. matlas is the open-source alternative: give it the raw descriptor, get back the merchant, a spending category, a confidence score, and the evidence trail (including the MCC, where one exists) that backs them up.

$ matlas enrich "SQ *STARBUCKS #4521 SEATTLE WA"
{
  "raw": "SQ *STARBUCKS #4521 SEATTLE WA",
  "region": "US",
  "rail": "card",
  "merchant": "Starbucks",
  "category": "food_and_drink",
  "confidence": 0.95,
  "consistency_check_applicable": true,
  "consistency_ok": true,
  "evidence": [
    {"source": "llm", "detail": "Starbucks", "confidence": 0.9},
    {"source": "resolver", "detail": "gazetteer_exact:5814", "confidence": 1.0}
  ],
  "is_unknown": false
}

Why it's built as an agent (and why that matters)

Most LLM wrappers ask the model to guess a category and call it a day. The problem: models are confidently wrong about merchants, and there's no way to tell a good guess from a bad one.

matlas splits the job in two:

  1. The LLM proposes. It reads the raw descriptor and suggests a merchant and category — the thing LLMs are genuinely good at (untangling SQ *, store numbers, city suffixes).
  2. A deterministic resolver checks. A curated merchant gazetteer (exact match, then fuzzy match) looks up the authoritative category and MCC. This is a real tool call inside the agent loop — a lookup table, never a second LLM guess.

When the two disagree, a validator (core/validator.py) catches the contradiction and sends it back: the model sees the resolver's evidence and gets exactly one bounded retry to reconsider. If the resolver has high confidence, its answer wins. Every returned record carries the evidence trail, so you can see why matlas believes what it believes.

The result: hallucinations get caught by a lookup table instead of shipping to your ledger.

Two regions, one honest difference

The core is region-agnostic; regions plug in as swappable RegionPacks.

  • US — the benchmarked pack. Card descriptors resolve against a 330-row gazetteer with real, iso18245-verified MCC codes. This is where accuracy claims live, because MCC gives us ground truth to measure against.
  • India — the portability pack. UPI narrations (UPI/DR/.../SWIGGY/...) are parsed, merchant payees resolve against a 186-row gazetteer, and person-to-person transfers get carved out as personal_transfer (there's no meaningful "category" for paying your friend back for dinner). India ships without an accuracy claim — no public labeled corpus of Indian bank narrations exists, so any number we printed would be made up. The pack demonstrates that the architecture ports; it doesn't pretend to a benchmark that can't exist yet.

That asymmetry is deliberate. We'd rather tell you what we can't measure than invent a number.

Install

pip install -e ".[dev]"       # library + CLI + tests
pip install -e ".[api,dev]"   # + REST API / MCP server

You need an ANTHROPIC_API_KEY in the environment for anything that runs the agent loop (CLI, API, MCP). The offline test suite (pytest tests --ignore=tests/live) needs no key — the agent loop is tested against replayed response cassettes, which is also what keeps CI green without secrets.

Using it

CLI:

matlas enrich "SQ *STARBUCKS #4521 SEATTLE WA"
matlas enrich "UPI/DR/408123456789/SWIGGY/YESB/Payment for order" --region india

--region accepts auto (default — detected from the descriptor's shape), us, or india.

Bulk CSV — the realistic path. Nobody enriches one transaction at a time; you have a statement export or a feed dump. Point matlas at a CSV with a descriptor column and it runs every row through the cost-tiered batch path (exact gazetteer hits never touch the LLM — a file of known merchants costs nothing and needs no API key):

matlas enrich-csv transactions.csv --column descriptor --out enriched.csv

Output is the input CSV with merchant, category, confidence, and is_unknown columns appended. Bank gave you a PDF? Export the CSV from your net-banking portal instead — every major bank offers it, and PDF table extraction is a different problem than transaction enrichment.

REST API (matlas serve --api, FastAPI on :8000):

POST /enrich         {"descriptor": "...", "region": "us"}    -> EnrichedTransaction
POST /enrich/batch   {"descriptors": [...], "region": "us"}   -> [EnrichedTransaction]
GET  /healthz

The batch endpoint is cost-tiered: an exact gazetteer hit skips the LLM entirely (free), a fuzzy hit gets confirmed by a cheap model, and only genuinely ambiguous descriptors escalate to the strong model. On realistic batches, most rows never touch the expensive path.

MCP server (matlas serve --mcp, stdio transport): exposes one tool, enrich(descriptor, region=None), returning the same structured record — plug it into Claude Desktop or any MCP client and let the model enrich transactions mid-conversation.

Measuring it

The eval harness runs the gold set (the same curated gazetteer rows, 330 US + 186 India) through the full agent loop and reports per-field accuracy plus mean tool calls per transaction:

from matlas.eval.gold import load_gold
from matlas.eval.harness import run_benchmark
from matlas.eval.report import render_report

render_report(run_benchmark(agent, load_gold()))

Judging defaults to exact match; an LLM-as-judge (cheap model, one-turn same-merchant check) is available for fuzzy merchant-name comparison.

The report includes a confidence-calibration table — predictions bucketed by reported confidence, each bucket's mean confidence side by side with its actual accuracy. When matlas says 0.9, that table is how you check it was right about nine times in ten. Calibration here is measured, never tuned blind. India results are labeled a portability smoke test in the report output, never an accuracy benchmark — see above.

Privacy stance

Descriptors you enrich are sent to the Anthropic API and nowhere else. matlas keeps no telemetry, no logging of your data, and no server-side anything — it's a library. The gazetteer lookup happens locally. If a descriptor resolves with an exact gazetteer hit through the tiered batch path, it never leaves your machine at all.

Contributing

The highest-leverage contribution is gold-set rows: real (anonymized) descriptor shapes, especially Indian bank narrations, where no public corpus exists. Second-highest: a new region pack — the whole point of the RegionPack seam is that your country's rails plug in without touching the core. CONTRIBUTING.md walks through both, including the data-honesty rules (every US MCC verified against iso18245, no invented benchmarks).

Status

Weeks 1–3 of the build plan complete and verified end-to-end against the live API: agent loop, US + India packs, CLI, REST API, MCP server, cost-tiered batching, LLM-judge, eval harness, CI. Week 4 (launch polish, PyPI release) in progress. Design docs and the full build log live outside this repo.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

matlas-0.1.0.tar.gz (41.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

matlas-0.1.0-py3-none-any.whl (33.1 kB view details)

Uploaded Python 3

File details

Details for the file matlas-0.1.0.tar.gz.

File metadata

  • Download URL: matlas-0.1.0.tar.gz
  • Upload date:
  • Size: 41.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for matlas-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4bb708bda00572630613d695305a0cb0335df84ca531bc96d96f860df18adcfa
MD5 ac266d84ad1b7a0ec3156d08a847fc19
BLAKE2b-256 599bc5cb9efb672942912770bcec7fca2f11660627f16bfec161122c46dc5750

See more details on using hashes here.

File details

Details for the file matlas-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: matlas-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 33.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.6

File hashes

Hashes for matlas-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4c2287c9257f185b40ddd9caad77958dbe559156d4d2bb5376e0d051e8cca09b
MD5 0c3dcf8bcf52705a7a7ed1e538352348
BLAKE2b-256 d05704b03f0da3bbd4b1fb7674cb77b557d0360dfbe987d3b00d451230461aef

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page