-
-
Three answers, never two. Verified, unconfirmed, and ruled out with the requirement that decided it.
-
Fifteen minutes against a ten minute limit, and still not a rejection. That ride was measured from the neighbourhood, not this address.
-
Six requirements, three sources, every one dated. The rent from the listing, the ride from a routing engine, the hours from a map.
-
Every source it used and every one it rejected, with the reason. 13,662 searches the obvious way, 76 planned, 32 spent.
-
Fourteen minutes measured on real roads, not a straight line between two pins. The dashed line is the half mile gym check.
-
The plan is on screen before the first search is made, and nothing is called verified while the run is still going.
-
One more minute on the bike adds two homes. Priced from rides already measured, so asking costs nothing.
-
One function fronts every billed call: ledger, then cache, then a ceiling the caller cannot raise.
-
A task re-asks every saved question at four in the morning. Nine searches the first time, none the second, and nothing had moved.
Inspiration
Ask for somewhere to live in one sentence. A budget, a bedroom count, twenty-five minutes by bike to a named address, a gym open before six, laundry in the unit, a grocery open past ten. Every part is checked against whoever actually holds that answer: the rent from the listing, the ride from a routing engine, the hours from a map index. What comes back is three lists rather than two. What cleared everything, what was ruled out and by which requirement, and what could not be checked at all. Those last two are different answers, and a tool that merges them has started lying to you.
We built it because a friend took a summer internship at Adobe in Times Square and ended up across the river in New Jersey. The month before he moved went on questions no single website would answer: how long that commute really takes on a weekday morning, whether a grocery near the flat is open when he gets home, whether there is a gym before work. Every answer existed somewhere and none of them in the same place, so he did what everyone does. Fifteen tabs, and by the end no clear memory of which of the six places he liked actually had laundry in the building.
What it does
You ask in one sentence:
Under $2,800, one bedroom, no more than 25 minutes by bike to 2788 San Tomas Expressway, gym within half a mile open before 6 am, in-unit laundry, grocery open past 10 pm.
Six requirements, and no single source can answer more than three of them. Done the obvious way, letting the rental site filter on price and bedrooms and then checking the rest one listing at a time, that costs 176 API calls. Relokit answers it in 39.
It narrows before it spends. The site's own filters cost nothing and take thousands of candidates to twenty. One call per neighbourhood rules out whole blocks at once, leaving eight. Only those eight get an exact route and their own search for a gym and a grocery.
One home clears all six, and every value says where it came from:
Lincoln Arms Apartments, 560 Lincoln St, Santa Clara
found you asked for answered by
$2,800 under $2,800 Zillow
1 bed one bedroom Zillow
20 min by bike under 25 min by bike Directions
0.4 mi gym open before 6 am Google Maps
in unit laundry in-unit laundry Zillow
0.7 mi grocery open past 10 pm Google Maps
Fifteen are ruled out, each naming the requirement that decided it, rather than being quietly ranked lower:
263 E Hedding St, San Jose 46 min by bike
Blue Hills West Apartments at least 26 min by bike
Sara Vale Apartments, Sunnyvale 26 min by bike
That last one misses by a single minute, which is why the run also prices what loosening a requirement would buy, using rides it has already measured and paying nothing more:
no more than 25 minutes by bike
26 min unlocks 2 more homes
35 min unlocks 4
37 min unlocks 6
Four more could not be settled at all, and those are the interesting ones. They are not rejections.
Three more things about that run:
- You watch it happen. The plan is drawn before the first call is made and each step fills in as it reports, so the screen shows what happened rather than a bar walking toward a number already decided.
- A long list becomes a decision. When a looser question verifies a dozen homes, the app marks any home that another beats on every measured count at once and names what beat it, leaving the set where each option is better at something.
- Anyone can reproduce it offline, with two commands and no API key, because every provider response it needs is committed to the repository. The 431 tests run the same way, in about a second.
The same machine answers "somewhere to eat within a mile of Santana Row, open past 10 pm." A second demo run for restaurants is committed alongside the rental one and the same invariants are asserted against both. It handles two anchors at once, as in "1 bed within 3 km of Diridon Station and 3 km of Santana Row," because requirements compose rather than filling a fixed rental form. Twelve kinds of place today, and a test fails the build if a housing word reaches somebody who asked about a restaurant, because the rental vocabulary outlived the rental product once already.
Why the verified list can be trusted
What makes an answer worth trusting is what happens when a check cannot be completed.
Every requirement resolves to pass, fail, or unknown, and unknown is its own answer rather than rounded into a rejection. A rent quoted as $2,495+ against a $2,800 cap is genuinely unknown. So is a provider that times out. Rejection therefore requires that a source actually answered and the answer failed:
verdict = fail AND eval_state = evaluated
A failed call can never remove a place from your search. Anything unsettled is shown as exactly that, with the fact named. It's pinned by a test called "rules nothing out on a check that could not be made," and we watched it hold during a live backend failure mid-demo: verified places degraded to unconfirmed, and the rejection set didn't change by one entry.
What that looks like on screen
The three lists are the interface, not a summary of it.
- Every fact names its source, its date, and links back, because the point of checking against a source is that you can check the check.
- One click shows the working, including the sources considered and passed over, and the arithmetic that passed them over.
- The empty state names what could not be checked, rather than showing a blank panel next to a confident number.
- Assumptions are shown, not applied quietly. "Near me" has no universal distance, so the reach follows the subject, a walk for dinner and a neighbourhood for a home, and it appears as a requirement you can see and change. "Cheap mexican" becomes a price level, marked as inferred rather than as something you said.
- The unknown rule runs all the way through. A home with no stated rent is never removed by a price filter, and a place with no rating is not a bad one.
Eleven invariants are asserted against a whole answer in the shape the screen receives it, across both demo runs: that no place is listed twice, that each sits in exactly one list, that every fact shows its source and its age, that no undefined or NaN ever reaches the screen, and that nothing is ruled out on a check that could not be made. Every fault we found by hand was something the engine computed correctly and the interface then said badly, and none had broken a unit test of the part that produced them.
The planner
A cost-based optimizer in the database tradition, pointed at web APIs instead of tables. Each source is a capability row with a cost in calls, a granularity, a coverage estimate and a selectivity prior. Candidates are scored on eliminations per call:
score = ((1 - selectivity_prior) * coverage) / (cost_units * entities_requiring_evaluation)
Five things do the real work, and two of them are the halves of filter and refine, the pattern spatial databases use to avoid running an expensive check on everything:
Predicate pushdown. Price cap and bed count are pushed into the Zillow query itself, so the provider filters for free. The candidate set drops before a single billed call is made, and the page budget goes from 111 to 2.
A straight line certifies a miss. Straight-line distance is a hard floor on any real route, so if the crow flies further than your limit allows, no road can satisfy it and the candidate goes with zero calls. It can never certify a hit: a short line says nothing about the roads.
Turning a distance into a time needs a speed nobody beats, and guessing one would start rejecting reachable homes. So it is measured against every route any provider has ever described to us. The quickest recorded ride covers its own straight line at 4.33 metres per second; the bound allows 6, so a ride half again quicker than anything we have seen would still not be rejected. A test called "admissible against every recorded route" fails the build if anyone tightens it past the evidence.
A place already found certifies a hit. Every nearby search returns real coordinates, and a shop is where it is whoever asked. Those get pooled, so a gym found half a mile from one home settles the question for a neighbour it was never searched for, measured exactly, for no extra call. Absence from the pool settles nothing, because the pool is a union of whatever searches happened to return. So a home with nothing nearby in the pool is sent to a search aimed at its own address.
Granularity laddering. Region-level calls first, then one call per neighbourhood, then per-place calls last, against the smallest surviving set. Neighbourhood answers carry slack so a place is only eliminated when even the nearest point of its cell fails.
A binding fixpoint. Pure cost ordering is wrong, because some sources cannot execute until another has bound their inputs. Directions needs a destination point; producing one eliminates nothing, so a geocode scores zero on elimination power and loses every comparison it enters. Selection is therefore a fixpoint: each round admits what is currently executable, keeps the best candidate per requirement, and adds the bindings it produces. Which is why a geocode runs first: not because the tier order says so, but because in round one it is the only thing that can run at all.
176 calls using the provider's own filters and nothing else. 88 planned. 39 made.
That 176 is the fair comparison: what the same question costs if you let the rental site filter on price and bedrooms, then check everything else the obvious way, one listing at a time.
The 39 calls produced 108 recorded answers, because a neighbourhood search answers for every home in its cell and a straight line answers for free.
Those priors are measured rather than guessed. Each run records what every source actually did, and the next plan uses the measured value where it has one, says so on screen, and labels the rest as the estimate it is.
Why live search data is the point
A language model with web search can answer this for one apartment, and often will be right. The task is not one apartment. It is twenty, each carrying six requirements, and the answer is only worth anything if it is complete and every value is attributable to whoever said it. That is the shape of problem agentic search is measured on in WideSearch, where over ten systems including commercial ones were tested and the best scored 5 percent, against humans who reach close to 100 given time. The reported difficulty is not finding one page. It is collecting many specific values across many items and arranging them without quietly dropping some. Relokit treats that as a planning problem rather than a prompting one, which is why every fact it reports is fetched live from the source that owns it and carries the date it was fetched.
Seven engines, one integration:
zillow,zillow_property,google_maps,google_maps_directions,google_maps_reviews,yelp,google_news. Six providers that would otherwise mean six auth schemes, six response shapes, and six things to fix when one changes. One client, one key, and 20 capability rows across those seven engines, six of them free because they read facts out of a call already being made. Three answer this particular question, one more is enabled for a signal it does not ask for, and three sit switched off in the registry, which is all that turning a source off costs here.Reading what an engine returns is the work. Wiring it up is the easy half.
- Opening hours arrive as display strings, where the dash is an en dash and the space before AM is a narrow no-break space. A parser written against a hyphen and an ordinary space matches nothing, and every opening time comes back unknown while looking like missing data rather than a broken parser.
- Google drops the meridiem from the opening time when both ends share one, so "6 to 11 AM" is a morning and "4 to 8:30 PM" is an afternoon. The closing time has to be read first to know which.
- SerpApi echoes the request back inside the response metadata, and its callback URLs carry credentials in a query string. So committing a recorded response means scrubbing the body as well as the parameters, and the redaction step runs before anything reaches disk rather than after.
google_localsits in the code with no capability row, because it ignores thellparameter and answered for the wrong city.
Engines are data. Each is a registry row the planner reads at plan time, so a source that stops earning its place gets dropped by the arithmetic, and adding a vertical means adding a row. Treating search endpoints as a costed capability set for a query optimizer is the part we would defend as genuinely original.
One search answers for many homes. Because the places a search returns are located facts, a search bought for one address answers for its neighbours too. On a dense Jersey City question that turned a handful of neighbourhood searches into two hundred and seven answered requirements at no further cost.
A ledger, not a cache. The response is cached per call, but each fact extracted from it is kept separately with its own expiry, so a repeat question pays only for what has actually gone stale.
At-most-once on every billed call. A metered call is a side effect that costs money, so a retry is not free the way a retry usually is: a call that fails after the provider has already answered has been billed, and asking again buys it twice. We learned that by doing it. Unmetered calls now retry with backoff and jitter; billed ones get exactly one attempt, and a failure comes back as
unknownrather than as a second purchase.
The AI use case here is a system whose defining decision is where the model is not permitted to go. One model call turns a sentence into typed constraints, and even its numbers are re-derived from the source text, because it once read "open past 10 pm" as 36,000 seconds, which is 10 am. The planner, the executor and every verdict are deterministic, and every fact on screen names the source that answered it and the day it did.
What it would cost to run
The cost of a new question is bounded and enforced, not estimated. At most 32 live searches, refused inside the backend rather than trusted to the client. Most hackathon projects cannot say what a request costs them; this one refuses to exceed it.
The ledger compounds across people, not just sessions. Facts are stored per fact with their own expiry, so a fact bought for one person answers the next person's question free. A price keeps two days, opening hours for a month. Every question asked in a city makes the next one there cheaper, and the second morning of a saved question is close to free. That advantage grows with use, and it is built rather than planned.
A new vertical is a row. Sources are registry rows with a cost, a coverage estimate and a granularity rather than code paths, which is why one engine already answers for twelve kinds of place.
Multi-tenancy already exists, which is the difference between relocation and mobility teams being a hope and being a customer. Org resolution is a shared function, every read is scoped to the owning org, and each org has its own ceiling. What the planner learns is stored against a hash of the place asked about, never the typed address, and a question anchored on "your location" is never pooled with anyone else's, because the first question a mobility team asks is what happens to their people's addresses. And the planner is sellable on its own: the MCP server exposes two tools that make no network calls at all, so another agent can price a question before deciding whether to spend anything answering it.
The structural risk is supply. Candidates come from a rental site through an intermediary, and that could change. Sources being rows means the planner starts choosing a replacement as soon as it earns its place, with no change to the planner or the executor, though it is not free: reading a new provider's response shape is code, one mapper per provider. Three sources sit in the registry today, wired and switched off, which is what that costs.
Challenges we ran into
An optimisation deleted an answer instead of deleting work. Twice.
The first time, someone asked for a ten minute bike ride and got back nothing verified, with no mention of the ride anywhere on the page. Two parts of the system had each concluded the other was handling it. One found a cheaper way to answer that requirement and stopped scheduling the expensive per-place checks; the other then worked out the cheaper way was not worth its cost and skipped it too. Neither was wrong on its own terms, and the requirement fell through the gap between them.
The second time was in the arithmetic that chooses between sources. Sources are ranked by how many places they eliminate per search, which is the right question while there is a choice and the wrong one when the alternative is not asking at all. A cheap source that answers without eliminating loses that comparison, and when the expensive winner turns out to be unaffordable, nothing takes its place. On a dense question with two hundred candidates, two requirements were therefore never measured. A place is only verified when every requirement passes, so the verified list came back empty while the tool insisted it had done its job.
Both are one rule now: an optimisation may remove cost, never an answer. Anything still unanswered once everything has been priced takes the cheapest source that can answer it, whatever its elimination power, and any requirement the plan never reaches is written on every place as unchecked rather than quietly dropped. On that same question it was the difference between nothing verified and four, with not one additional rejection.
The backend is not transactional, so we measured instead of assuming. A job queue built on a guess about whether a write survives a failed request is a queue that loses work. We wrote a small endpoint that inserts a row and then deliberately fails, and checked. The row survives. That one measured fact is what makes the queue safe: an attempt is recorded before the work runs, so a poll that dies still counts against its job, and a job that keeps failing is passed over after two tries instead of looping forever.
The slowest thing in the product was a query we wrote ourselves. A question whose answers were all already stored still took five minutes and twelve seconds, and the gateway timeouts in production were its tail. Before spending anything, the system checks what it already knows, and that check was loading every unexpired fact in the account, around eighteen thousand rows, then filtering in application code to find the two it actually needed. Moving both conditions into the database query, where an index answers them, took the same question to sixteen seconds.
Accomplishments that we're proud of
Shipping the third list. A ranked list would have been far easier, and people would have assumed the ranking meant something. Saying "we could not confirm this, and here is which fact" is harder to build and harder to sell, and it is the only version we would trust when signing a lease.
What we learned
We were wrong three times running about things we would have bet money on. Retries are free: they aren't. The slow lookup is a missing index: the index existed, and our own query was the problem. An ad blocker is eating the listing photos: it was a signed map image refusing our domain. Each took ten minutes to measure, and none was where we looked first.
What's next for Relokit
More runs in more regions, since learned priors need volume. Then comparison between saved places, and coverage beyond where the current sources are strong.
Build story, for the Xano challenge
What software did we replace? The saved search. Every rental site has one, it filters on price and bedrooms and almost nothing else, and it mails you listings that satisfy the two requirements it understands while saying nothing about the four you actually care about. So the moment your requirement is "and twenty-five minutes by bike, and a gym open before six," the tool hands the work back to you and you become the integration layer between three products that will never talk to each other.
Why that one? Because we watched it done by hand, and because the failure is structural. Nobody was being lazy. No single company owns the answer to a multi-part question about a place, so no single company can fix it.
What Xano actually did. The backend lives in the repository as 56 files of XanoScript, versioned and reviewed in the same diffs as the TypeScript, which is how we found a query that was costing five minutes a run: a where clause in a file is something you can read.
- One function is the only thing that can spend money.
Relokit/execute_opasks three questions in order: do we already know this fact for these listings, possibly from somebody else's question; did we already make this exact call; and only then may it buy. Two conditions guard the last one: the ceiling the run was accepted on, and a fixed upper bound the caller cannot raise, because the page is open to anyone and every search is billed. Past it, everything degrades to unconfirmed, never to a rejection. The same function serves the reader and the four in the morning task, so there is one spending rule rather than two that drift apart. - The AI runs inside Xano. Parsing is a Xano AI agent on the instance's included model. The prompt lives in a table rather than in the agent's configuration, because it is an input we change and want to review like any other: it is seeded from the repo, alongside the 20 capability rows the planner reads. So the repository is the source of truth, and Xano is the runtime, for the data as well as the code. An NVIDIA NIM fallback covers the case where the included credits run out mid-demo.
- A background task is the product.
relokit_watchre-runs every saved question nightly and reports what moved. It cannot live in a browser, because nobody has the tab open at four in the morning. - Auth and tenancy. Org resolution is a shared function, and every read is scoped to the owning org, with its own ceiling.
- A job queue. Work is enqueued in one request and drained by short polls, with attempts written before execution, so a call that outlives its poll is finished by the next one instead of being lost.
That is 16 tables, 16 endpoints of our own on top of Xano's auth starter, 4 shared functions, 1 background task, and 1 AI agent.
Three things the platform taught us, each by costing something:
- A
|getwith a default returns that default for a value that is merely falsy, not just for a missing one. Four homes advertise a rent band rather than a price, so both sides of the nightly comparison read them as absent, and the watch reported the same four homes arriving and the same four leaving, every night. Storing the value as an object fixes it, because an object is never falsy. - Xano preserves insertion order when it encodes JSON, so the cache key for a call depended on the order the parameters were written. The warmer wrote the engine last, and the executor wrote it first; nothing ever matched, and we spent 38 real searches proving it. There is one shared hashing function now, so the two cannot drift.
- Requests queue rather than running alongside each other, which we assumed rather than measured for one afternoon. Six calls in flight took the median call from 2.6 seconds to 25.9 and the whole run from 205 seconds to 258, for the same work. Asking for parallelism only moved the waiting.
Which AI tools did we use? Claude Code for the build. Inside the product, the AI is Xano's own agent, as above, and the one thing we never let a model do is arithmetic: every number it returns is re-derived from the words the user typed.
How long? Five days of building, 28 August to 1 September, about 150 commits, with two or three days before that on the decisions that are expensive to change later: the cost model, reading what each engine actually returns rather than what its docs claim, and finding out what Xano could do. The scoring formula and the three-valued verdict rule were both settled before the first stage ran, which is why neither had to be rewritten.
What would have taken far longer without AI and Xano? The backend. A job queue, a per-fact ledger with real expiry, org scoping, and a spend ceiling, all built, hosted, and secured, would have taken the whole five days on their own and the planner would never have been written.
On hosting. The page sends an empty org key and a server-side passage injects the real one, so no credential ever reaches a browser, and the public page is bounded by a server-side spend ceiling rather than by trust. That injection needs somewhere to run, which is the one thing static file hosting cannot offer, so only the static files sit outside Xano. The data, the AI, the queue, the nightly task, and the money all stay in it.
Built With
- claude-code
- maplibre
- nvidia-nim
- pwa
- react
- serpapi
- typescript
- vercel
- vite
- vitest
- xano
- zod
Log in or sign up for Devpost to join the conversation.