Inspiration
Renting a flat in an Indian metro often starts with a lie. Ads show photos of a different flat, promise a "5 min walk to the metro" that is really 20, and hide extra charges until signing day. Tenants pay a cash "token" to book a visit and often lose it. Brokers drive across the city for tenants who never show up. Each side only has the other's word.
We asked one question: what if every promise in a rental deal had to be proved, and the money moved only when the proof agreed? PayPal already has the tools to act as the referee: authorization holds, invoices and disputes. We built PropLock around them.
What it does
PropLock follows one flat from the ad to the paid move-in invoice, in four features:
- Verified listings. Before an ad goes live, it is checked against facts the broker can't change:
- A · Photos: check-in photos must be taken at the flat (GPS within 100 m) in the last 30 days, and must not be reused from another flat.
- B · Commute: travel claims are checked against rush-hour routes.
- C · Deposit: the deposit is checked against state rules, with the source cited.
- D · Owner terms: the ad must match the rent range and deposit the owner agreed to.
A passing report is signed (HMAC) and published as a PayPal Catalog product. Anyone can check that the report wasn't edited afterwards.
- A booking assistant. It answers a tenant's questions only from the signed audit. It refuses discount requests and prompt injection. It collects budget, move-in date and profile, then holds a visit slot for 15 minutes, with a travel gap between the broker's visits at different flats.
- Visit escrow. The visit fee is a PayPal Orders v2
AUTHORIZEhold: held, not charged. At the door, the broker types the tenant's 4-digit PIN, and both phones can check in by GPS. A fixed set of rules then decides:
| What happened | PayPal |
|---|---|
| Tenant visited and passed | void: $0 |
| Tenant locked the flat and the broker approved | capture: becomes the move-in token |
| Broker was at the flat, tenant never came | capture: no-show fee |
| Tenant was at the flat with a live photo, broker never came | void, and the listing is taken down |
| Nobody proved anything by the deadline | void |
- Invoice ledger. When a tenant locks the flat, a PayPal invoice goes out for exactly the verified amounts, minus the visit token:
$$ \text{total} = \text{deposit} + \text{brokerage} - \text{token} = 70{,}000 + 35{,}000 - 15 = 104{,}985 $$
Every money event lands in an append-only ledger, shown in an AG Grid cockpit. You can ask it questions in plain English. When a tenant opens a dispute, a copilot drafts a response that cites PropLock's own signed records, and a person approves it before anything is sent.
How we built it
- Backend: FastAPI, SQLAlchemy 2 and Alembic (SQLite for the demo, Postgres-ready). The four listing checks run in parallel in a LangGraph pipeline.
- AI: Groq for reading claims out of ad text, comparing photos, the booking assistant, the ledger copilot and dispute drafts.
- PayPal: Catalog Products, Orders v2 (
intent=AUTHORIZE, capture, void), Invoicing v2, verified Webhooks and Disputes. Every PayPal call goes through an outbox with a fixedPayPal-Request-Id, so a retry replays PayPal's first answer instead of acting twice. - Frontend: React, Vite and TypeScript, with a phone-first broker app, a web desk for the agency, and public tenant pages. The theme is dark midnight and gold, with a light mode.
- Quality: 474 backend tests, plus demo scripts that run each feature's whole story on a throwaway database and fail if any step behaves differently.
The visit fee is dearer the closer the visit, so booking early pays off:
$$ \text{fee}(h) = \begin{cases} 0.8 \times \$15 & h \ge 48 \ \$15 & 12 \le h < 48 \ 1.3 \times \$15 & h < 12 \end{cases} \qquad h = \text{hours until the visit} $$
Challenges we ran into
- Keeping the AI honest. An LLM will happily say "you're ready to book" to a tenant whose budget is below the rent. So the model only suggests, and our own rules re-check every answer; they can only overrule it toward "not ready". "Couldn't check" (no API key, no route data, an unsure vision model) is never turned into a pass. It goes to a person.
- Double booking. Two tenants clicking the same slot at the same moment must not both win. We lock the broker's calendar row before reading it, and a four-thread race test proves exactly one hold is created.
- Anti-cheat edge cases. Our own demo found a loophole: a broker could claim a no-show from home and have it "proved" later by checking in at the flat. Now the claim only counts if the broker's GPS at the flat came first. Testing also caught a no-show fee being offered as a move-in token, which would have let another tenant's invoice deduct it.
- Money that never counts twice. Webhooks get replayed, jobs get retried, and a manual refresh can report the same payment twice. Every ledger entry has a unique idempotency key, and the ledger rejects updates and deletes.
- Currency and data limits. The PayPal sandbox has no domestic INR between Indian accounts, so invoices are in USD while the verified ₹ figures are shown alongside. OpenStreetMap has no live traffic, so driving claims it can't settle go to a person instead of being guessed.
What we learned
- Make the AI write proposals, not decisions. Every place an LLM touches gets a deterministic rule after it.
- PayPal's authorize-then-capture model fits an escrow problem. It covers the honor period, idempotent request ids, and void as the safe default.
- Write the anti-cheat rules as a pure function first. Table-driven tests then cover each way a visit can end before any UI exists.
What's next
- Run the full approve → authorize → capture/void flow with a sandbox buyer. Order creation is already confirmed against the sandbox.
- Deliver webhooks to a public URL.
- Pay no-show fees out to brokers with PayPal Payouts.
- Add a public "explore verified flats" feed.
Built With
- ag-grid
- alembic
- fastapi
- groq
- llm
- openstreetmap
- paypal
- paypal-disputes-api
- paypal-invoicing-api
- paypal-orders-api
- paypal-webhooks
- pillow
- pytest
- python
- react
- react-router
- sqlalchemy
- sqlite
- typescript
- vite