AI-powered income tax notice management for CA firms. Upload a notice PDF → 4-agent AI pipeline → get a legally drafted response in minutes.
- Python 3.11+
- Node.js 18+
cd backend
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install dependencies
pip install fastapi 'uvicorn[standard]' sqlalchemy alembic aiosqlite \
'python-jose[cryptography]' 'passlib[bcrypt]' python-multipart \
pymupdf pdfplumber pikepdf langgraph langchain-core langchain-mistralai \
mistralai httpx pydantic pydantic-settings aiofiles boto3 python-dotenv
# Configure environment
cp ../.env.example .env
# Edit .env and add your MISTRAL_API_KEY
# Seed demo data
python db/seed.py
# Start API server
uvicorn main:app --reload --port 8000cd frontend
npm install
# Create .env.local
echo "NEXT_PUBLIC_API_URL=http://localhost:8000" > .env.local
npm run devThe frontend app is a standard Next.js project and works out-of-the-box on Vercel:
- Push this repo to GitHub.
- In Vercel, Create New Project and select the repo.
- Set Root Directory to
frontend/. - Keep the default build settings (
npm install,npm run build,npm startnot needed for Vercel). - Configure environment variables in Vercel:
NEXT_PUBLIC_API_URL=https://your-backend.example.com
- Deploy.
The Next.js rewrite infrontend/next.config.tswill send all/api/*requests to your backend:
// frontend/next.config.ts
const nextConfig = {
async rewrites() {
const backendUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000';
return [
{
source: '/api/:path*',
destination: `${backendUrl}/api/:path*`,
},
];
},
};
module.exports = nextConfig;In production, set
NEXT_PUBLIC_API_URLto the public HTTPS URL of your backend (see below).
The backend already ships with a production-ready Dockerfile and a docker-compose.yml you can adapt for your infra.
- Provision a VM with Docker + Docker Compose.
- Copy this repo (or just the
backend/directory) onto the VM. - Create a
.envon the host based on.env.exampleand set at minimum:APP_ENV=productionDATABASE_URL=postgresql://user:password@host:5432/ledgeraiJWT_SECRET=<strong-random-secret>MISTRAL_API_KEY=<your-mistral-key>ALLOWED_ORIGINS_STR=https://your-vercel-domain.vercel.app,https://yourcustomdomain.com
- Use
docker-compose.ymlas a reference:- Run Postgres as a managed service or keep the bundled
postgresservice for smaller deployments. - Run the
backendservice built frombackend/Dockerfile.
- Run Postgres as a managed service or keep the bundled
- Put Nginx/Caddy/Traefik in front of the backend container to terminate HTTPS and expose:
https://your-backend.example.com/api/*→http://backend:8000/api/*
-
Build and push the backend image:
cd backend docker build -t your-registry/ledgerai-backend:latest . docker push your-registry/ledgerai-backend:latest
-
Create a service in your provider of choice:
- Image:
your-registry/ledgerai-backend:latest - Port:
8000 - Health check:
GET /health(expects JSON withstatus: "ok"or"degraded").
- Image:
-
Attach a managed Postgres instance and set
DATABASE_URLaccordingly. -
Configure environment variables matching
.env.example:APP_ENV=productionJWT_SECRET,MISTRAL_API_KEY,RESEND_API_KEY, storage (USE_S3,AWS_*),ALLOWED_ORIGINS_STR,REDIS_URL(optional).
-
Expose the service over HTTPS using a managed load balancer or custom domain; use that URL as
NEXT_PUBLIC_API_URLin Vercel.
| Field | Value |
|---|---|
demo@ledgerai.ai |
|
| Password | ledgerai2026 |
Add your Mistral API key in backend/.env:
MISTRAL_API_KEY=your_key_here
Get a key at console.mistral.ai
Without a Mistral API key: The 4-agent pipeline will run but return fallback default responses instead of AI-generated content. Seeded demo data with pre-generated drafts will work without any API key.
frontend/ (Next.js 14, Tailwind CSS)
app/
page.tsx Landing page
login/ Auth pages
register/
dashboard/ Protected app
page.tsx Dashboard stats
notices/ Notice management
[id]/ Notice detail + approval
upload/ Upload PDF + trigger pipeline
clients/ Client PAN management
intelligence/ Resolution Intelligence
lib/api.ts Type-safe API client
backend/ (FastAPI, Python 3.11)
main.py FastAPI app entrypoint
core/
config.py App settings
auth.py JWT authentication
db/
models.py SQLAlchemy ORM (7 entities)
schemas.py Pydantic API schemas
database.py DB engine + sessions
seed.py Demo data seeder
services/
pdf_service.py PyMuPDF + pikepdf PDF processing
mistral_service.py Mistral AI client (classify, draft, strategy)
cross_ref_service.py 26AS × ITR cross-reference engine
agents/
graph.py LangGraph 4-agent StateGraph pipeline
├── classifier_node Extract & classify notice fields
├── analyst_node Build ProofObject via cross-reference
├── strategist_node Recommend resolution strategy
└── drafter_node Generate legal response letter
api/routes/
auth.py /auth/login, /register, /me
clients.py /clients CRUD
notices.py /notices upload, status, approve
dashboard.py /dashboard/stats, /intel
Upload PDF
│
▼
[Agent 1: Classifier] ──── Mistral extracts: section, AY, amount, deadline
│
▼
[Agent 2: Analyst] ──── Cross-references vs 26AS + ITR → builds ProofObject
│
▼
[Agent 3: Strategist] ──── Mistral recommends: PAY / FILE_154 / REVISED / CONTEST
│
▼
[Agent 4: Drafter] ──── Mistral drafts legal response letter + doc checklist
│
▼
CA Approval Card ──── CA reviews proof + draft → Approve / Edit / Escalate
| Entity | Description |
|---|---|
Firm |
CA firm account |
User |
Firm members (Partner / Senior CA / Article) |
Client |
Taxpayer PAN + DOB (for PDF decryption) |
Notice |
Central notice object with status lifecycle |
ProofObject |
Evidence map: notice claims vs our records |
ResolutionOutcome |
Resolution Intelligence learning record |
AuditLog |
Immutable action log for every decision |
- Passwords hashed with bcrypt
- JWT auth (7-day expiry)
- Firm-level data isolation
- PDF files stored locally by default (configure S3/R2 via
.envfor production) - All portal notice PDFs are decrypted using PAN+DOB — never stored plaintext
- Environments
APP_ENV=development: local dev, docs enabled, relaxed logging.APP_ENV=production: stricter startup checks, docs hidden, HSTS enabled, warnings for missing AI keys.
- Secret rotation
JWT_SECRET: rotate by updating the value in your secret manager and restarting the backend; all existing sessions are invalidated.MISTRAL_API_KEY,RESEND_API_KEY,AWS_*: update in your provider’s secret store and restart containers.
- Monitoring health
GET /health: used by load balancers; returns DB and AI status and currentapp_env.- Watch for
PIPELINE_FAILEDentries inaudit_logswhen debugging notice pipeline issues.
- Logs
- Backend uses Python logging plus structured audit logs (
audit_logstable) for key actions:NOTICE_UPLOADED,PIPELINE_COMPLETE,PIPELINE_FAILED,DRAFT_APPROVED,DRAFT_EDITED,ESCALATED_MANUAL.
- For production, forward container stdout/stderr to your log platform (CloudWatch, Loki, Datadog, etc.).
- Backend uses Python logging plus structured audit logs (
- Notice pipeline failures
- When the AI pipeline crashes, the notice is marked
FAILEDand an audit log entry is created. - Typical remediation steps:
- Check raw PDF in storage for corruption.
- Verify
MISTRAL_API_KEYand upstream AI availability. - Re-upload the notice after fixing configuration.
- When the AI pipeline crashes, the notice is marked
Alembic is already included in backend/requirements.txt and can be adopted for schema evolution:
- Create an Alembic config in
backend/and point it atdb.database:Base. - Generate an initial migration from the existing models.
- For new schema changes, generate and apply migrations instead of relying on
create_all.
This is recommended before large-scale production rollout, but the current setup is sufficient for early-stage deployments.
LedgerAI is software infrastructure, not a legal advisor. The CA firm holds all responsibility for submitted responses. LedgerAI mirrors Tally's established liability model.
LedgerAI © 2026 — Confidential. YC Application Stage.