Install, then run the demo. No API key is required. The matcher is rule-based and prints matched pairs, unmatched rows, and drafted discrepancy text.
pip install -e .
reconcile demoSame sample files, one shot:
python examples/run_sample.pyor reconcile run examples/settlement.csv examples/ledger.csv.
To turn on the optional LLM layer, set LLM_MODEL and the provider's own key. LiteLLM reads that key from the usual env var. Local Ollama needs only the model string.
| Provider | Model string | Env |
|---|---|---|
| OpenAI | gpt-4o or gpt-4o-mini |
LLM_MODEL + OPENAI_API_KEY |
| Anthropic | claude-opus-5 or claude-sonnet-4-5 |
LLM_MODEL + ANTHROPIC_API_KEY |
gemini/gemini-2.0-flash |
LLM_MODEL + GEMINI_API_KEY |
|
| OpenRouter | openrouter/openai/gpt-4o |
LLM_MODEL + OPENROUTER_API_KEY |
| Ollama (local) | ollama/llama3.2 |
LLM_MODEL only |
| vLLM | hosted_vllm/<model> |
LLM_MODEL + OPENAI_API_KEY + OPENAI_API_BASE |
# OpenAI
export LLM_MODEL=gpt-4o-mini
export OPENAI_API_KEY=sk-your-key
# Anthropic
export LLM_MODEL=claude-sonnet-4-5
export ANTHROPIC_API_KEY=sk-ant-your-key
# Local Ollama (Ollama must already be running)
export LLM_MODEL=ollama/llama3.2Then run reconcile demo again. With no key set, the agent stays in rule-based mode.
A small Python library and CLI that takes a settlement or payout file (what the gateway says it paid you) and an order or invoice ledger (what you expected), then returns matched pairs, unmatched rows on both sides, and a classified list of gaps.
Keywords: payment reconciliation, settlement matching, payout reconciliation, finance ops, ledger matching, fee mismatch, short-paid, transaction matching, razorpay settlement recon, invoice matching
Give reconcile two files: a gateway settlement report and your own ledger. It pairs rows by transaction id first, then by amount and date within a tolerance you set. For every pair it computes amount, fee, and net differences. Rows that still do not pair are listed as missing on one side. Duplicates and currency clashes are flagged as their own kinds. The matcher is deterministic and runs with no API key. An optional LLM tool loop (any provider, via litellm) can propose a match for leftover fuzzy rows and rewrite explanations; if no provider key is set, the rule-based explanations already produced by the matcher are kept. Version 0 only reads reports. It does not capture charges, send payouts, or call a live payment API.
Finance teams hit this when a bank credit does not equal the sum of invoices: MDR and tax ate part of the gross, a payment is short, a refund landed in the same settlement, or the gateway id never made it into the order system. Dashboards show the gateway's view. Spreadsheets VLOOKUP on payment id and miss the rest. This project is the matching engine for that job, written as a tested library rather than a sheet.
| Approach | What you get | Where it stops |
|---|---|---|
| Spreadsheet VLOOKUP / XLOOKUP | Pairs on payment id if both files share that column | Misses amount+date matches, fee gaps, duplicate ids, and a stable JSON report |
| Gateway dashboard / settlement recon export | Totals, UTR, per-transaction fee and tax from the processor | Does not know your invoice ledger, so it cannot say what is missing on your side |
| Full ERP / accounting suite | Posted journals after someone maps the file | Heavy to stand up for a two-file check; still needs a matcher for exceptions |
| This library | ID match, then amount+date match, then classified discrepancies and a CLI report | Read-only. No live payouts, no auto-posting to books |
The same exception list (short-paid, fee mismatch, missing, duplicate, currency) shows up in payment operations work, including the settlement recon task that processor agent studios describe. The code here is gateway-agnostic. CSV and JSON work for any processor. A Razorpay recon loader understands the public combined-settlement field names and converts integer subunits (paise) to major units.
The path through the code is linear.
-
Load.
load_settlement()andload_ledger()insrc/reconcile/sources/__init__.pypick a format. Generic CSV and JSON accept aliases such aspayment_id,gross,settled_at, andinvoice_id.load_razorpay_settlement()insrc/reconcile/sources/razorpay.pymapsentity_id,amount,fee,tax,credit,debit,settled_at,settlement_id,settlement_utr, andorder_idfrom the public recon payload, dividing integer subunits by 100. Nothing in this package opens a payment API socket. -
Match by transaction id.
match()insrc/reconcile/matcher.pycalls_match_by_transaction_id(). Rows that share a non-emptytransaction_idare paired one-for-one in file order. Extra copies of the same id on one side becomeduplicate. -
Match leftovers by amount and date.
_match_by_amount_date()pairs remaining rows when currency matches, both dates exist,abs(amount gap) <= amount_tolerance(default0.01), andabs(day gap) <= date_tolerance_days(default1). If two ledger rows qualify, the closer amount wins; a tie takes the earlier ledger index. This pass is greedy and never reuses a row. -
Classify each pair.
classify_pair()/pair_lines()storeamount_diff,fee_diff, andnet_diff(netdefaults toamount - fee - tax). Gaps larger than tolerance becomeamount_mismatch(short-paid or over-paid),fee_mismatch, orcurrency. Unmatched settlement rows aremissing_in_ledger. Unmatched ledger rows aremissing_in_settlement. -
Optional explanations.
ReconcileAgent.enrich()insrc/reconcile/agent.pyruns only when a provider key is present (or the model is a local Ollama/vLLM id) andRECONCILE_DISABLE_AGENTis not set. It is a short litellm tool loop (list_unmatched,list_discrepancies,propose_match,set_explanation) onLLM_MODEL(defaultgpt-4o-mini). A refusal or content filter discards agent output and keeps the matcher result. On any client error the same fallback applies.
MatchResult.to_dict() is the JSON report. Money is serialized as decimal strings, not floats.
Python 3.10 or newer.
git clone https://github.com/pandeyvishwas51-oss/reconcile.git
cd reconcile
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"Runtime dependency: litellm (called only if you enable the agent). Optional extra: pydantic (pip install -e ".[pydantic]") if you want it in the same environment for your own wrappers. The matcher itself uses dataclasses and the standard library.
Generate a planted sample (clean matches, a short-paid row, a fee gap, a currency clash, an amount+date pair, orphans, and a duplicate) and print the table plus JSON:
reconcile demoWrite the sample CSVs to a directory you choose:
reconcile demo --dir ./demo-data --no-jsonRun against your own files:
reconcile run path/to/settlement.csv path/to/ledger.csvRazorpay combined recon JSON (amounts in paise) against a ledger in major units:
reconcile run recon.json ledger.csv --settlement-format razorpay --ledger-format csvUseful flags:
reconcile run settlement.csv ledger.csv \
--amount-tolerance 0.05 \
--date-tolerance-days 2 \
--fee-tolerance 0.50 \
--json-out report.json \
--no-agentLibrary use, no CLI:
from decimal import Decimal
from reconcile import MatcherConfig, load_ledger, load_settlement, match
settlements = load_settlement("settlement.csv")
ledger = load_ledger("ledger.csv")
result = match(
settlements,
ledger,
MatcherConfig(amount_tolerance=Decimal("0.01"), date_tolerance_days=1),
)
print(result.summary())
for item in result.discrepancies:
print(item.kind.value, item.explanation)Sample files from reconcile demo --dir ./examples are enough to see every discrepancy kind the matcher emits.
Generic settlement CSV/JSON (major units):
transaction_id, order_id, amount, currency, date, fee, tax, net, settlement_id, utr, description
Generic ledger CSV/JSON (major units):
transaction_id, order_id, invoice_id, amount, currency, date, fee, tax, net, description
Accepted aliases include payment_id / entity_id / txn_id for the id, gross for amount, settled_at / unix timestamps for the date, and mdr for fee.
Razorpay recon (JSON items array or CSV with the same headers): amounts, fees, tax, credit, and debit are integer subunits. The loader divides by 100.
pytestCI runs pytest on Python 3.10-3.13 with provider keys cleared. Matcher and loader tests use fixtures under tests/fixtures/ (including the public recon sample from the Settlements Recon docs). The agent tests mock litellm.completion; they never open the network.
Does this move money? No. There is no capture, refund, transfer, or settlement call in this package. It reads two reports and writes a match result.
Do I need an LLM key?
No. Matching and rule-based explanations run without one. Set LLM_MODEL and the provider's own key (OPENAI_API_KEY, ANTHROPIC_API_KEY, ...) only if you want the optional fuzzy-match / rewrite loop. Local Ollama needs only LLM_MODEL=ollama/llama3.2. Tests use the dummy value sk-test-dummy with litellm.completion mocked.
Which gateways work? Any processor if you can export CSV or JSON with an id, amount, currency, and date. The Razorpay loader is a convenience for the public recon field names. Stripe, PayPal, Adyen, or a bank payout file work through the generic loaders once the columns are named or aliased.
Why is my Razorpay amount 100 times too large?
The recon API and many recon dumps store paise. Use --settlement-format razorpay (or a filename that already looks like recon) so the loader divides by 100. Generic CSV/JSON keep major units.
What does short-paid mean here?
The rows paired (usually by transaction id) but settlement.amount - ledger.amount is below -amount_tolerance. The pair stays in matched; the gap is an amount_mismatch discrepancy.
Can two rows with different ids still match? Yes, on pass 2, if amount, currency, and date sit inside the tolerances. That is how a missing gateway id still lines up with an invoice of the same value posted a day later.
What if the model refuses? A refusal or content-filter finish drops agent proposals and keeps the deterministic result, including the original explanations.
Is pydantic required?
No. Models are dataclasses. The pydantic extra is optional for callers who already use it.
MIT. See LICENSE. Cite with CITATION.cff.