Inspiration
Every rental listing is a set of claims. The rent, the parking space, the move-in date, whether it is even still available. Most of those claims are stale, some are optimistic, and a few are simply wrong. The only way to find out is to call the agent and ask, which means the same eight questions, over and over, for every property on your shortlist.
That is the job nobody wants and a phone agent is genuinely good at. So Propster does the calling, and more importantly, it does the checking.
What it does
You describe what you want in a sentence. Propster turns that into a typed requirement, searches the listings, and ranks what it finds. You choose which properties are worth a phone call. Then CALL-E rings the listing contact, introduces itself as an AI, asks about availability, rent, amenities and every upfront fee, and hangs up politely when asked.
What comes back is a transcript. Propster extracts structured facts from it, validates them against a strict schema, and scores the property across six weighted components: availability, price accuracy, required amenities, property details, additional fees, and contact confidence. Anything the agent said that contradicts the advert is surfaced as a discrepancy, with the line from the call that proves it.
The point is not the call. The point is that every answer has a source.
How we built it
Next.js 16 on the App Router with React 19 and TypeScript in strict mode, Prisma against Neon Postgres, and Tailwind v4. Calls go through the @call-e/calle SDK, and the app follows each one to a terminal state using the unsigned terminal webhook when it is publicly reachable and polling when it is not.
Two decisions shaped the architecture.
The first is that phone verification sits behind a provider interface. There is a live CALL-E provider and a deterministic simulated one, both returning the same shapes and driving the same state machine. Listings carrying numbers from ranges reserved for fiction, such as the Ofcom drama range, route to the simulated provider automatically, so a judge can explore the whole product without a single telephone ringing. Every simulated call is labelled as such in the UI and stored with its provider recorded, because a demo that quietly pretends to make calls is worthless.
The second is that nothing a model says is trusted. This CALL-E plan rejects both result_schema and recipient_result_schema, so structured extraction happens in the app rather than server-side. Every extracted fact is parsed against a Zod schema before it is used, there is a deterministic rule parser
behind the model as a safety net, and a fact that cannot be established comes back as null rather than as a guess.
Challenges we ran into
The model invented a rejection. On a real call, the contact answered "I don't have information about apartment availability, so I can't verify that for you". The extraction model turned that into property_available: false, and the app told the user the property was no longer on the market. Nobody
had said that. The prompt already forbade guessing and the prompt was not enough, so the claim is now checked against what was actually spoken: a "no longer available" survives only if a contact turn really says the property has been let, taken or withdrawn. Otherwise it degrades to unconfirmed. The
failure direction is the whole point, because an unconfirmed property is merely unproven while a falsely unavailable one is wrong.
Seventy-four invisible bytes. While fixing that guard I noticed a regex was not matching text it obviously should. A \b word boundary had become a literal 0x08 backspace character, so /\beur\b/ was really /\x08eur\x08/ and could only match a string containing an actual backspace. Scanning the
repo turned up seventy-four more. The entire currency table and the rent-period detector had been dead the whole time. The test suite had stayed green because every existing case used a symbol like the euro sign, and the bare symbols were the one part of each pattern carrying no escape. The lesson
was not about the bug, it was about the tests: they had only ever exercised the half of each pattern that could not break.
Telling one failure from another. A call that is declined, one that rings out, and one where the voice agent never starts all look similar from a distance and mean completely different things to a user. Reading the SIP status ahead of the provider's prose, and the prose ahead of the error code, is what separates "nobody picked up" from "the number refused the call" from "no call was placed at all".
Regional calling restrictions. Partway through the build, calls to the number used for testing stopped connecting. Three failures that looked unrelated turned out to be one cause: the platform had tightened which region and language combinations it supports. Nothing in the app could fix it, but the app was hiding it, reporting a permanent restriction as a temporary outage and inviting retries that could never succeed. It now reads the provider's own explanation and says plainly that no call was placed and nobody was contacted.
What we learned
Speech recognition output from a live phone line is the real adversary. Real transcripts from this project include "there is preparing me sir" for "there is a prepaid meter, sir". A regex parser cannot survive that, which is why a language model does the reading, and why everything it produces is validated before it counts.
More broadly: when a system's entire value is telling you what is true, every piece of it has to be built to under-report rather than over-claim. Saying "we could not establish this" is a feature.
What's next
Real listing feeds through partners whose terms permit it, callbacks so a contact can return the call, and multilingual verification now that the extraction seam is provider-agnostic.
Built With
- call-e
- groq
- neon
- next.js
- node.js
- postgresql
- prisma
- qwen
- react
- tailwindcss
- typescript
- vercel
- vitest
- zod
Log in or sign up for Devpost to join the conversation.