IMS reconciliation and supplier-chase assistant for Indian CA firms.
Finds the GST input credit a firm's clients are about to lose, and chases the suppliers responsible — while a correction is still possible.
Since IMS became mandatory for all GST taxpayers, every supplier invoice lands on the recipient's dashboard and has to be actioned Accept, Reject or Pending. Only accepted documents flow into GSTR-2B. No action by the GSTR-3B deadline counts as acceptance.
The trouble is the calendar:
Day 11 Day 14 Day 20
suppliers GSTR-2B GSTR-3B
file GSTR-1 generates due
│ │ │
└─────────────┴──────────────────┘
└──── 6 days ──────┘
to clear hundreds
of invoice lines
Most firms do this work between the 14th and the 20th. By then it is too late to fix anything — a supplier can only amend through GSTR-1A before the 2B generates. So a wrong invoice becomes Reject, and the client's credit is deferred by a month or lost.
It moves the work from days 14–20 to days 3–13, and chases the supplier instead of the client.
Reconcile early, spot the supplier who has not filed on the 3rd, message them while a GSTR-1A amendment still fixes it. The pitch to a CA is not "we show you mismatches" — every tool does that. It is "we recover your clients' blocked credit", and that is measurable in rupees.
It never touches the GST portal. No automation against a government website, no stored portal credentials, no captcha solving. Programmatic filing requires a licensed GSP, and browser automation against a government system is a bad idea for reasons that go well beyond terms of service.
The system reads exports, finds problems, recommends actions, and drafts
messages. A chartered accountant approves everything. That boundary is the
product's whole trustworthiness, and it is enforced in code — see
src/Veridex/approvals/risk.py.
git clone <your-repo> && cd Veridex
pip install -e ".[dev]"
# Make some realistic test data
python scripts/generate_synthetic_data.py --invoices 400
# Run it
uvicorn Veridex.main:app --reloadOpen http://localhost:8000 for the dashboard, or http://localhost:8000/docs for the API.
No database to install, no API keys, no Docker. It runs on SQLite with a mock LLM out of the box.
cp .env.example .env
docker compose upPostgres, Redis, the API and a worker. Migrations run automatically.
| Metric | Value | What it means |
|---|---|---|
| Match rate | 93.1% | share of portal lines paired with a book entry |
| Automation rate | 75.8% | share the CA can bulk-approve without reading |
| Tests | 87 passing | including full API flow and tenant isolation |
| Eval suites | 4/4 passing | with CI gates that fail the build on regression |
The 100% classification accuracy in the eval suite is circular. The synthetic data is generated using the same assumptions the matcher uses. It demonstrates the code works; it demonstrates nothing about real exports.
Real purchase registers are far messier than anything simulated here. Expect the match rate to drop substantially on first contact with reality — that is normal, and the config file exists precisely so you can tune it back up.
| Gap | Detail |
|---|---|
| Auth is a placeholder | get_current_user trusts an X-Tenant-Id header. Anyone reaching the API can claim to be any firm. Replace before a second firm uses it. |
| Column names are guesses | The adapters in ingestion/adapters.py use published GST formats. Some will be wrong. This is the first thing to fix with a real export in hand. |
| Sarvam endpoint unverified | Written to the OpenAI-compatible convention, not tested against live docs. |
/cycles/{id}/reconcile |
Parsed records live in memory for one request. Use upload-and-reconcile (what the dashboard does) or wire object storage. |
| OAuth needs credentials | The flow is complete and CSRF-protected, but needs a Google Cloud project. |
config/matching_rules.yaml
Almost everything a CA tells you about how decisions get made lands there, not in Python:
- how invoice numbers get normalised before comparison
- amount tolerance (currently ₹1 or 0.1% — confirm this)
- which mismatch maps to Accept / Reject / Pending / Chase
- what needs human approval (the risk tiers)
- when to chase suppliers and how often
- quiet hours and rate limits
If you find yourself editing Python to change a threshold, stop and add it there instead.
IMS export Purchase register
(portal) (Tally / Busy / Excel)
│ │
└──────────┬──────────┘
▼
ingestion/adapters.py ← swap this when formats differ
│
▼
matching/engine.py ← NO AI. deterministic. auditable.
│
┌──────────┴──────────┐
▼ ▼
clean matches problems (~25%)
│ │
│ ▼
│ agents/graph.py ← LLM explains + drafts, decides nothing
│ │
└──────────┬──────────┘
▼
approvals/risk.py ← THE GATE. nothing passes unapproved.
│
▼
the CA decides
Matching thousands of invoice lines is arithmetic and string comparison. A language model would be slower, more expensive, non-deterministic and impossible to audit.
When a CA asks "why did you match these two lines?", the engine answers with an exact rule. A model cannot. The LLM's job is narrow and downstream: explain the leftovers in Hindi, and draft the supplier message.
Knowing where a model does not belong is the interesting engineering decision in this project.
# Tests
pytest tests/ -v
# The AI quality gate (also runs in CI)
python -m Veridex.evals.run
# Fresh test data
python scripts/generate_synthetic_data.py --invoices 500 --period 082026
# Migrations
alembic revision --autogenerate -m "what changed"
alembic upgrade head
# Background worker
python -m Veridex.worker
# MCP server (for Claude Desktop)
Veridex_TENANT_ID=your-firm python -m Veridex.mcp_server.server
# Lint
ruff check src/ tests/config/matching_rules.yaml ← the business rules. edit this.
src/Veridex/
ingestion/ adapters + normalisation ← swap when formats differ
matching/ the tiered engine ← the heart. no AI.
agents/ LLM explanation + drafting ← writes prose, decides nothing
approvals/ risk tiering ← the safety boundary
evals/ metrics + CI gate ← quality under regression test
queueing/ memory / redis / sqs
messaging/ whatsapp
integrations/ google oauth + token vault
observability/ tracing, metrics, cost tracking
mcp_server/ tools for Claude Desktop
api/ HTTP endpoints (thin)
services.py business logic (thick)
web/static/ the dashboard, one HTML file
tests/ 87 tests
scripts/ synthetic data generator
- Replace the auth placeholder. Not optional.
- Set
ENCRYPTION_KEYif you connect Google. The vault refuses to store plaintext tokens in production, but check it. - Rotate
SECRET_KEY— it signs the OAuth state. - Switch
QUEUE_BACKENDtoredisorsqs. The memory queue loses jobs on restart. - Run migrations rather than
create_all. - Get the CA to sign an NDA before he shows you a single client file, and mask PAN/GSTIN/names on your own machine. Client confidentiality is his professional obligation, not a formality.
- Check ICAI rules before offering a practising CA equity in this.
Everything discovery-dependent is isolated behind matching_rules.yaml and
the adapter layer, so your CA's answers plug in without touching code.
But the go/no-go question is still open:
Can the CA actually export the IMS dashboard, in what format, and does it break above a certain row count?
The supplier-side view reportedly restricts downloads above 500 documents. If the answer is "no export at all", the ingestion layer changes shape entirely.
That is a fifteen-minute conversation and it is worth having before building anything further on top of this.