Inspiration

Every year DIAN publishes, for each taxpayer, the información exógena: a spreadsheet of what employers, banks and brokers reported about you. Turning it into a correct Formulario 210 means chasing certificates, catching silent zeros, and doing arithmetic where one wrong bracket flips the result from "you owe" to "you are owed". Accounting firms have tooling; a salaried person filing their own return has a PDF instruction manual. RentaLista started with one question: can an agent gather the evidence and prepare a traceable draft, while a human keeps every decision and nothing is ever filed automatically?

What it does

  • Ingests the real DIAN exogenous layout (multi-banner XLSX with header discovery and threshold rows) plus a bank certificate PDF.
  • Decides whether you must file with the five AG-2025 thresholds (UVT 49,799) using the exact > / >= operators from the DIAN instructions.
  • Calculates a Form 210 draft with a deterministic engine: integer pesos only, no floats, no LLM arithmetic. Every cell carries its formula, operands and rule version, and presentation amounts are rounded to thousands as art. 577 requires.
  • Shows the evidence in a bilingual (es/en) UI: profile, document inventory, coverage, and a draft with a cell ledger under the disclaimer Borrador para revisión. No ha sido presentado ante la DIAN.
  • Never files, signs or pays. It does not log in to DIAN.

The progressive tax is a pure function of the taxable base \(b\) in UVT. For the art. 241 bracket \(i\) with lower bound \(L_i\), fixed tax \(F_i\) and marginal rate \(r_i\):

$$\text{tax}(b) = F_i + (b - L_i)\, r_i \qquad L_i < b \le L_{i+1}$$

The table lives in a versioned rule pack (rules/ag2025/) and unit tests assert it against the statute, including continuity at every bracket edge.

How we built it

  • Engine: Python 3.13, Pydantic models, COP = NewType("COP", int), Decimal only at the ingestion boundary, an explicit case state machine with human-in-the-loop states (PORTAL_APPROVAL_REQUIRED, BROWSER_USER_ACTION_REQUIRED, NEEDS_REVIEW). 52 tests: unit, golden snapshot, and a test that fails if the UI's demo JSON drifts from the engine output.
  • Agent: a structured command envelope (PREPARE_DRAFT, FIND_MISSING_DOCUMENTS, RESUME_AFTER_USER_ACTION, ...) that rejects free-form prompts. The entrypoint runs on Amazon Bedrock AgentCore Runtime as a CodeZip; Strands Agents wraps the engine as a prepare_draft tool so a model can orchestrate but never compute. On the public demo path the entrypoint calls the engine directly.
  • Document recovery: a seeded portal directory with deliberate decoys (lookalike domain, URL shortener, paid third-party portal) filtered by a deterministic policy, and a search-query guard that refuses PII. AgentCore Gateway (Web Search) and Browser Live View with OTP handoff are provisioned as the next step.
  • API + web: FastAPI on AWS Lambda behind CloudFront /api/v1/*, Next.js 15 static export on S3 (en/es), case tokens hashed server-side, idempotent jobs, CSP with frame-src for the AgentCore Live View.

Challenges we ran into

  1. Correctness over vibes. Our first art. 241 table had wrong fixed amounts: at exactly 1,090 UVT it charged 19 UVT instead of zero. A continuity test at every bracket edge caught it, and fixing it flipped the synthetic case from "amount payable" to "credit balance". Tax rules need tests against the statute, not against our own snapshot.
  2. The real DIAN layout. The exogenous report has banners, "Tope 1–5" threshold rows and repeated NIT columns. Header discovery and float-safe money parsing (str() → Decimal → integer pesos) came before any number could be trusted.
  3. Two AWS accounts and an SCP. An organisation policy blocked CreateFunction in the member account for most of the hackathon, so the API ran in one account while the AgentCore Runtime, Gateway and Browser lived in another.
  4. Keeping the UI honest. The draft page shows a JSON generated by the engine, and make demo-draft plus a test keep it identical to what the engine computes.

What we learned

  • Put the LLM where judgment is needed (orchestration, explanations, asking the human) and keep money in pure functions.
  • Human-in-the-loop is a state machine, not a prompt: approval, consent and OTP handoff are first-class states with explicit transitions.
  • A public demo needs a synthetic-data policy from day one: fictional reporter, seeded portal, OTP 123456, no real taxpayer PII.

What's next

Wire the Gateway Web Search and the Browser Live View OTP handoff into the public flow, load the YAML rule pack at runtime, cover the remaining Form 210 sections, and validate the golden case against DIAN's Programa Ayuda Renta.

Built With

  • amazon-api-gateway
  • amazon-bedrock
  • amazon-bedrock-agentcore
  • amazon-cloudfront
  • amazon-web-services
  • aws-lambda
  • fastapi
  • mangum
  • mypy
  • next.js
  • openpyxl
  • pydantic
  • pypdf
  • pytest
  • python
  • react
  • ruff
  • strands-agents
  • tailwindcss
  • typescript
  • uv
Share this project:

Updates

Submission history