DEV Community

Toheeb Olanrewaju Olagoke
Toheeb Olanrewaju Olagoke

Posted on

Building VeraMove: An AI That Calls Three Movers, Catches Hidden Fees, and Negotiates a Better Deal.

28 million Americans move every year, in a $20B+ market made up of over 16,000 small moving companies. Real quotes for one identical 45-mile move have been documented ranging from $1,158 to $6,506, a 5.6x spread for the same job. Sight-unseen phone estimates are 40% more likely to end in a bill above the original quote. That's the problem our four-person team set out to solve at a recent Hack-Nation × ElevenLabs hackathon, and this is the story of how we built VeraMove: a voice-and-document intake pipeline that locks a single move specification, calls three vendors with it, catches the fees they don't mention up front, negotiates one quote against another, and hands back a ranked, evidence-backed recommendation. The core loop: The whole product boils down to one sequence, and we treated it as the single thing that had to work before anything else mattered:

The full source is public: github.com/zukhriddingit/VeraMove; it's a mock-first hackathon starter, no API keys or accounts required to run it yourself.

Voice or document intake

→ confirmed, version-locked JobSpec

→ three parallel vendor calls

→ itemized quotes with hidden-fee detection

→ negotiation using a verified competing quote as leverage

→ ranked recommendation with transcript and recording evidence. Everything else UI polish, awards strategy, video scripts- was explicitly secondary. "A working call beats a polished interface" became something close to a team motto. Architecture: mock-first, contract-driven FastAPI owns the canonical Pydantic contracts and generates the OpenAPI schema. APP_MODE=mock wires in an in-memory repository and deterministic synthetic fixtures, so the entire loop- three vendor calls, quote generation, negotiation, recommendation- runs without a single external API key or account. That decision mattered enormously for a 24-hour build: nobody was blocked waiting on ElevenLabs quota or an OpenAI key while the demo loop got proven out.

The frontend's rule, spelled out in the repo's AGENTS.md, was strict: one API client, types generated from FastAPI's OpenAPI schema via openapi-typescript, never handwritten parallel domain models. Any contract change meant: update the Pydantic models and backend tests, re-export the OpenAPI JSON, regenerate the TypeScript types, update call sites, run the full check suite, then get sign-off from both the backend and frontend owners. That discipline is what let four people build in parallel without four different mental models of what a Quote looks like. Four conversation-design requirements, made visible. Because "moving services negotiator" is a fairly obvious vertical for a hackathon like this, differentiation had to come from execution depth, not novelty. One deliberate choice: build a visible checklist in the UI for the four things the challenge brief actually cared about in a voice agent.


Disclosure: every call opens by stating it's an AI calling on the customer's behalf, and answers "are you a robot?" honestly. Friction survival: at least one call has to survive real friction (hold music, a rushed dispatcher) and land a structured callback instead of a dead end. The honesty line: the agent can use a real verified competing quote as leverage, but must never invent inventory or fabricate a competitor's number. Structured close: no call ends on a vague answer; every call closes as an itemized quote, a callback commitment, or a documented decline.

We built this as a small four-item card that lights up based on real call outcome data rather than being pure set dressing; two items are static guardrail statements (things baked into every synthetic call by design), and two are computed live from each call's actual outcome type. Honest > flashy, especially when honesty is literally what the requirement is testing for. Hidden fees and red flags, from the schema up. The vendor persona design does a lot of work here. Three synthetic vendors, three distinct negotiation styles: a transparent one that itemizes everything up front, a cheap-headline one that reveals fees only under direct questioning, and a premium one that moves meaningfully when shown a better verified quote.

On the data side, every fee line item on a quote carries a disclosed_upfront: boolean. Any fee where that's false is, by definition, a hidden fee caught after the fact, which meant the frontend didn't need any fuzzy logic to surface this, just a filter. Red flags work the same way: a red_flags: string[] field lives directly on both the quote and the final recommendation ranking, populated by the backend's own rules (e.g. a quote 30%+ below the median gets flagged, never presented as a clean win without a caveat). The mid-build pivot: Vite/React to Lovable Partway through, after a role swap put me on frontend ownership, we shipped a fully working Vite + React + TypeScript implementation of the loop, intake, confirm, calls, negotiate, report, all wired to the real mock backend, with hidden-fee and red-flag surfacing, a full JobSpec review screen, and test coverage for the new logic. It passed the full CI-equivalent check (Ruff, pytest, OpenAPI export, typecheck, Vitest, production build) and went up as a PR.

Then the team decided to move the frontend to Lovable. That decision came with a real lesson in the cost of a mid-build tool switch:

Rebuilding on hearsay contracts. Lovable's agent can't run openapi-typescript against a live backend the way our Vite setup could; it works from whatever you tell it. My first pass described the API contract from memory, and it quietly diverged from the real schema in three ways: money fields (original_total, negotiated_total, deposit) are serialized by FastAPI as decimal strings, not JSON numbers; stairs is a numeric count, not a boolean; and version fields are the literal string "1.0", not a number. All three got caught and fixed, but only because we'd separately verified the real schema earlier in the Vite build; without that, they'd have shipped wrong. Verifying against real payloads, not assumptions. When we needed the shape of a call record for the conversation-design checklist, I initially had Lovable invent field names based on partial information. The actual backend owner sent a real JSON payload back, and the real shape looked meaningfully different: a full Vendor object nested in multiple places instead of a {id, name} shorthand, a status field, and started_at/completed_at timestamps. Type-correcting against a real payload took minutes; building on the wrong assumption and finding out later would have cost a lot more. Unrelated git histories. Lovable auto-provisions its own GitHub repo per project with its own root commit. Pushing that as a new branch onto the real repository worked at the git level, but GitHub's compare view came back with "there isn't anything to compare, main and this branch are entirely different commit histories." The fix was mechanical once diagnosed: branch from the real main, copy the Lovable-exported files into the correct directory, commit fresh on top of the real history, and push that instead. A five-minute problem once you know unrelated-history diffs are a known GitHub limitation, a confusing one if you don't. CORS and reachability. A cloud-hosted frontend can't call http://127.0.0.1:8000; that address only means something on the machine running it. Anything built in a hosted tool needs either a publicly reachable backend or a tunnel, and the backend's CORS policy needs to explicitly allow the new frontend's origin. Easy to forget when you've been developing against localhost for the whole build.

None of these were hard problems. All of them were the direct cost of losing the single source of truth (a generated, verified contract) the moment the frontend moved to a tool that couldn't generate types from the live API itself. The fix in every case was the same instinct: stop guessing, go get the real payload, verify against it. What we'd tell another team doing this: Freeze your contracts before writing feature code, and freeze them first between backend and frontend, not last. Build the ugliest possible version of the full loop before touching visual polish. A working call beats a polished interface, every time. If you have to build in a tool that can't generate types from your live API, verify field-by-field against a real payload before you build on top of assumed ones; the difference between "grounded in the generated contract" and "hand-typed from memory" is exactly the gap where subtle, expensive bugs live.

VeraMove is a hackathon starter, not a production product, mock mode only, synthetic data throughout, no real vendor calls. But the loop it proves one locked spec, three comparable quotes, one negotiation backed by real leverage, one evidence-linked recommendation is the whole idea, and getting that loop working end to end, twice, in two different frontend stacks, taught us more about contract discipline than either build alone would have.

Check out the code: github.com/zukhriddingit/VeraMove. Clone it, run python scripts/bootstrap.py, then python scripts/dev.py, and you'll have the full loop running locally in about five minutes no credentials needed

Top comments (0)