Inspiration

Landlords lose security deposit disputes in small claims court because they can't produce dated, comparable, statute-grounded evidence of what the unit looked like at move-in versus move-out. If the landlord miss the itemization deadline, or get a bad-faith finding, most state deposit statutes impose a penalty of two to three times the withheld sum, paid to the tenant.

Every existing tool we looked at, AppFolio, Buildium, TurboTenant, RentRedi, stores photos and tracks leases, but none of them reason about condition change, apply state-specific wear-and-tear law, or produce a deduction letter that could survive a challenge. That gap, between having photos and having a defensible case, is what MoveOutDocs is for. It's built for the self-managing landlord with 2 to 75 units who has no in-house counsel and no property manager, just a phone camera and a lease that's ending.

What it does

MoveOutDocs runs two photo sessions per unit: Move-In, taken at lease start, and Move-Out, taken the day keys come back. The Move-Out room list is inherited directly from Move-In instead of rebuilt from scratch, so the two sets are guaranteed to be comparable instead of hoping the landlord happened to photograph the same things twice. Each photo gets a server-side SHA-256 hash and an EXIF timestamp check. If EXIF is missing, which is common with gallery uploads that passed through a messaging app, the system falls back to the upload time rather than blocking the upload.

Once both sessions are complete, a six-stage pipeline takes over: intake integrity, visual comparison, wear-and-tear classification against the property's actual state statute, cost estimation, a mandatory landlord review, and document drafting. The landlord receives a list of itemized deductables and AI-generated pins over each detected change, with three possible outcomes per pin: normal wear, beyond-normal, or uncertain. Nothing reaches the final letter without the landlord accepting, rejecting, or editing it first, and uncertain items are never auto-included.

The output is a split view. The itemized deduction letter sits on one side, a reasoning ledger on the other, and every line item links back to its photo evidence, its classification, and its exact statute citation. It exports as a formatted PDF, plus a full evidence archive a landlord could hand a small-claims judge. A tenant also gets a single-use link to review the move-in photos and either acknowledge them or flag a disagreement. Both responses get recorded before the pipeline moves anywhere.

How we built it

We used Codex through the entire build, from the first architecture document to the last PDF layout fix. Codex wrote the 30-table Supabase schema, the Alembic migrations, the FastAPI services, and the Next.js capture and review UI, then went back in to fix its own bugs as we found them: a stage-advancement error in the pipeline worker, a JSON schema mismatch across all four LLM-backed agent services, a duplicated tenant name in the PDF greeting. Under a single codex session, we built the first end-to-end agent intake and integrity, including the typed SDK wiring, the audit trail, and three new tests, verified against a real test run.

The reasoning pipeline runs on ai models through the OpenAI Agents SDK, but the sequencing between stages is application code, not model-chosen handoffs. A coordinator we wrote in Python moves each finding through intake, visual comparison, statute retrieval, classification, cost estimation, a human review checkpoint, and drafting, in that fixed order, and records the trace ID, span ID, and input and output hash for every step. The statute agent is deliberately the least creative part of the system. It looks up structured, human-verified records for six states we researched by hand, Arizona, California, Florida, Oregon, Texas, and Washington, and returns not_supported for anything else instead of letting the model improvise legal text.

The stack is Next.js on the frontend, FastAPI and Postgres on the backend through Supabase, a background worker that claims pipeline stages with FOR UPDATE SKIP LOCKED so multiple workers never double-process a row, Resend for the tenant acknowledgment email, and ReportLab for the final PDF.

Challenges we ran into

The first real challenge was proving comparability. If a landlord photographs a kitchen from a different angle at move-out than at move-in, the comparison agent has nothing solid to reason about. We locked the Move-Out room list to whatever was defined at Move-In, custom rooms included, so the two sessions match by construction instead of by luck.

Getting the LLM-backed agents to return structured output reliably took a few passes. All four agent services had a JSON schema mismatch we didn't catch until integration testing, and one worker bug caused pipeline stages to advance out of order under retry. Both needed direct debugging rather than a quick patch.

The highest-stakes decision was refusing to let the model generate law. It would have been faster to ask GPT-5.6 to reason about wear and tear in Texas from its training data and call it done. Instead the statute agent only serves pre-verified, source-hashed records, and a state stays marked unsupported until a human has actually checked the primary source against what's in the database. That constraint shaped a lot of the schema and slowed the seeding work down, but it's the one part of the system we weren't willing to compromise on.

We also found and fixed a real authorization gap during testing. A signed-in landlord could read or edit another landlord's profile just by supplying its UUID. We caught it, added a regression test, and closed it before it became a demo-day surprise.

Accomplishments that we're proud of

The whole pipeline runs end to end against a real database and a real deployed instance, not a mocked demo path. As of the last build pass, the backend has 26 passing tests, the FastAPI app exposes 147 OpenAPI paths, and both the Alembic migration chain and the frontend production build pass clean.

We're proud that the uncertain classification actually behaves like a hard stop rather than a suggestion. If the statute agent can't find verified data for a jurisdiction, the classification agent refuses to classify at all, and no unpriced, unverified, or pending finding can reach the drafting agent. That's enforced both by a database check and by an Agents SDK guardrail, so it doesn't depend on one code path remembering to check. We also shipped the tenant side, not just the landlord tool: a real Resend-backed, single-use, expiring acknowledgment link bound to an immutable hash of the exact photo set the tenant saw.

What we learned

Building a sequential, code-owned coordinator instead of letting the model choose handoffs mattered more than we expected going in. It made every stage individually testable, and it's the reason we could catch and fix a worker bug in isolation instead of debugging one opaque prompt chain.

We also learned how much of trustworthy AI output comes from what you refuse to let the model do, rather than what you ask it to do well. The statute retrieval agent is the least flashy part of the system and the part we spent the most care on, because it's the one place a hallucination turns into bad legal advice.

On the Codex side, the clearest lesson was that giving it a narrow, testable unit at a time, one agent, one endpoint, one migration, produced better results than asking for a broad feature in one pass. Running compileall, pytest, and npm run build after every change became a habit rather than an afterthought.

What's next for MoveOutDocs

Statute coverage is the obvious next step. Six states proves the model works; it isn't enough to serve most of the landlords who need it. Beyond that, we want a real multi-property portfolio view for landlords managing more than one building, since the data model already supports property-to-unit hierarchies even though the hackathon UI only demos one unit at a time. We'd also like to bring small property management companies into the target user base, and tighten the visual comparison agent's handling of poorly aligned photos, since camera angle mismatch is still the most common reason a finding gets marked uncertain instead of classified.

Built With

Share this project:

Updates