We used a swarm of AI subagents, six MCP servers, and a lot of adversarial skepticism to plan a 12-night Maui trip. The agents didn't find the answer. They destroyed the wrong ones.
The goal was almost embarrassingly simple: two people, twelve nights on Maui, celebrating a birthday. Under $3,500 for housing. An ocean view. And a beach you could actually swim at — soft sand, no rocks.
That last constraint is the whole story. It's not a field in any database. No listing will ever tell you its beach is rocky. And it turned a weekend of browsing into a genuinely hard search problem — one that ended up consuming 17 multi-agent workflows, 249 agent runs, and 41 shipped versions of a self-updating trip dossier.
Three separate times, a "winner" was crowned, written into the plan, and published. Three times, a skeptic agent tore it down: one was already booked. One was "oceanfront" onto a shelf of lava rock. One had an owner-direct link that quietly 301'd to a different condo nine miles up the coast, so the "verified price" we'd been quoting was for a property that didn't exist at that URL.
The unit we finally booked was never the front-runner. Total cost of the exercise: less than one night of the condo. Here's the whole machine.
Everything ran inside Claude Code (Opus 4.8) as the orchestrator. The interesting part isn't the model — it's what it was allowed to touch.
| Tool | What it did | Why it mattered |
|---|---|---|
| Airbnb MCP | airbnb_search, airbnb_listing_details | Structured listing data — but not trustworthy for availability (see below) |
| Claude in Chrome | Real browser automation: navigate, read DOM, read console | The only way to get true availability + checkout-page totals. Airbnb 403s plain fetches. |
| DirectBooker MCP | Live hotel rates, book-direct links | Answered "are hotels actually cheaper?" with real numbers |
| TripAdvisor MCP | Ratings, review counts, cross-check pricing | Independent second source on hotels |
| Nominatim (OSM) | Geocoding condo addresses → lat/lng | Mapping. Also caught one badly geocoded address. |
| Web search + fetch | Independent beach guides, surf reports, jellyfish calendars | The ground truth listings never provide |
| Git + scp/nginx | Version + deploy the dossier on every change | The artifact is the deliverable |
The naive approach is to ask an LLM to search harder. That produces more plausible-sounding candidates, not more true ones. What actually worked was a two-phase shape, repeated over and over:
1. Fan out. Spawn N agents in parallel, each with a different search angle — one hunts owner-direct sites, one hunts Airbnb, one hunts timeshares, one hunts beach guides. Each returns a schema-validated object, not prose.
2. Attack. For every candidate that survives, spawn skeptics whose only job is to refute it. Not "evaluate" — refute. Default to rejection when uncertain.
flowchart TD
A["Constraints — the hard filters"] --> B{"Fan out"}
B --> F1["Finder: marketplace"]
B --> F2["Finder: owner-direct"]
B --> F3["Finder: adjacent categories"]
B --> F4["Finder: independent sources — build the missing column"]
F1 --> C["Candidates — schema-validated"]
F2 --> C
F3 --> C
F4 --> C
C --> D{"Adversarial verify — refute by default"}
D -->|PROBLEM| X["Killed"]
D -->|CONFIRMED| S["Survivors"]
S --> N["Prove the negative"]
N --> R["Synthesize into the dossier"]
R --> AU{"Self-audit for contradictions"}
AU -->|contradiction| R
AU -->|clean| RV["Re-verify at commit time"]
RV --> H(["Human decides"])
Here is the verify phase, verbatim from the file that ran (trimmed for length — the prompt was longer). It's JavaScript because the workflow runtime executes plain JS in a sandbox; this is not pseudocode:
// verify-kamaole.js — executed by the Workflow runtime
export const meta = {
name: 'verify-kamaole-winners',
description: 'Adversarially confirm the 3 finalists before crowning one',
phases: [{ title: 'Verify', detail: 'refute each candidate' }],
}
const CANDS = [
{ key: 'Royal Mauian', room: '50815427', claim: 'BEACHFRONT on Kamaole I, full ocean view, ~$3,423 all-in…' },
{ key: 'Kamaole Beach Royale #204', room: '21244792', claim: 'ocean-view lanai, ~3-min walk to Kam I soft sand, ~$2,556…' },
{ key: 'Maui Kamaole I-118', room: '22531397', claim: 'ocean-view lanai (filtered), Keawakapu ~5-10 min, ~$2,970…' },
]
phase('Verify')
const verdicts = await parallel(CANDS.map(c => () =>
agent(
`Adversarially verify this condo before we crown it.
Dates: 2026-07-25 -> 2026-08-06, 2 adults.
CLAIM TO TEST: ${c.claim}
Try to REFUTE each of these. Report what you actually find:
1) AVAILABLE for the exact dates? (active Reserve button?)
2) REAL ocean view from the unit? (photo/amenity evidence, not marketing fluff)
3) Exact ALL-IN price incl. Hawaii tax ~17-19%. Is it truly <= $3,500?
4) Nearest soft-SWIMMABLE sand + walk time. Rocky? Murky?
5) Red flags: construction, for-sale showings, no kitchen.
Final verdict: CONFIRMED (claims hold) or PROBLEM (what breaks).`,
{
label: `verify:${c.room}`,
phase: 'Verify',
schema: {
type: 'object', additionalProperties: false,
required: ['verdict','availableDates','oceanView','allInPrice','underCap','beach','redFlags'],
properties: {
verdict: { type:'string', enum:['CONFIRMED','PROBLEM'] },
oceanView: { type:'string', enum:['yes','partial','no','unknown'] },
underCap: { type:'string', enum:['yes','no','unknown'] },
availableDates: { type:'string' },
allInPrice: { type:'string' },
beach: { type:'string' },
redFlags: { type:'string' },
},
},
}
)
))
// → all three came back CONFIRMED. Two came back oceanView: 'partial'.
That last line is the point. The enum on oceanView is doing quiet, load-bearing work: it denies an agent the option of writing "stunning ocean vistas!" and forces it to pick one of four words. Two finalists whose listings both said "ocean view" resolved to partial — a strip of blue over a neighbor's roof, and a lanai filtered through landscaping. Free-text output would have laundered both into a yes.
(The diagram above is live Mermaid — the source is in the repo.)
This is the part worth reading. Every one of these cost us a crowned winner.
We crowned a beachfront unit, wrote it into the plan, and shipped. A later 19-agent availability re-check found it was booked — and had been for a while. So had our backup.
Lesson: availability is not a property of a listing. It's a property of a moment. Re-verify at decision time, not search time.
Load a sold-out listing with dates in the URL and Airbnb quietly serves you a different property — same page shape, different unit, real price. A scraper happily reports that price as the original unit's. We got burned twice before we detected it.
Worse: an owner-direct site (mauiownercondos.com/listing/68) 301-redirected to an entirely different condo on a different beach, 9 miles away. Our "verified $2,925 owner-direct price" was for a unit that didn't exist at that URL.
Lesson: always assert that the page you landed on is the thing you asked for. Check the title, the coordinates, the room ID. A 200 OK means nothing.
One finalist was genuinely oceanfront — fronting a shelf of lava rock. Another had "ocean views" that turned out to be a strip of blue visible over a neighbor's roof. A third sat on soft sand whose water runs murky and silty because it's next to a pond outlet.
The fix was to stop reading listings entirely for this dimension. We cross-referenced every property's coordinates against independent beach guides and review corpora, and built our own ranking: Keawakapu ≈ Kamaʻole I/II (soft, calm, sandy-bottom) > Kamaʻole III > Charley Young > Sugar Beach (soft to walk, murky to swim).
Lesson: when the deciding attribute isn't in the data, the job isn't search — it's constructing the missing column.
After enough iterations, the dossier contradicted itself: the hero section crowned a unit flagged disqualified three sections down, while a booking checklist told you to buy a $6,204 condo that violated the stated $3,500 cap. Nobody wrote that bug. It accreted.
We fixed it with an 8-agent staleness audit: chunk the document, give each agent the current ground truth, and ask "what in your chunk contradicts this?"
Lesson: a long-lived generated artifact needs a consistency checker as much as any codebase needs tests.
perl -pi a UTF-8 file with wide-char replacements. It double-encoded the entire document — every emoji and ʻokina turned to mojibake. Python with explicit encoding='utf-8', always.integrity= attribute that doesn't byte-match makes the browser block the script with no console error you'd notice. Our custom map rendered as an empty box.Late in the process came the best question of the whole project: "Is a condo really the best value? Is there nothing else?"
This is exactly the question a search engine can't answer, because it requires exhausting a space rather than ranking within one. So: six agents, one per lodging category, all pricing live for the exact dates.
| Category | Best found | Why it lost |
|---|---|---|
| Timeshare weeks | ~$5,571 | Sat–Sat blocks force a 14-night two-week stitch for a 12-night trip |
| Whole house near a swim beach | $4,200–6,900 | Over budget; cheap cottages sit far from soft sand |
| Hotel + car bundle | ~$3,272+ | Marks up the same condo class, and isn't beachfront |
| Book a month (28-night discount) | ~$4,700–5,000 | Monthly discount is only 10–13% — nowhere near 16 free nights |
| Glamping / North-Shore cottage | $2,682 | Romantic and cheap, but the swim beach is a 20-minute drive |
Verdict: the condo wins, and now we know why — it's the only category that satisfies ocean view and soft swimmable sand and a whole private place under the cap. Every cheaper option drops one leg. That's a far more useful answer than "here are 40 listings."
Maui Kamaole I-118 — 1BR/2BA, 1,000 sq ft, two lanais, full kitchen, A/C.
$2,937 all-in for 12 nights (Hawaii tax included, no cleaning fee). 4.92★ / 185 reviews, Guest Favorite.
Directly across the road from Keawakapu — the highest-rated soft-swim beach in South Maui (4.8★, 570 reviews).
It was never the crowned pick — the agents ranked it third on their own shortlist, behind a beachfront splurge and a cheaper unit. But once the swarm had eliminated everything that couldn't survive scrutiny, a human looked at the nine survivors and overrode the ranking: space + the best water over paying $500 more to stand on sand ten seconds sooner. Which is exactly the call the machine couldn't make for me — it can rank facts, but it can't weigh what a birthday is worth.
That's the division of labor I'd defend: agents narrow, humans decide. The machine's job was never to pick. It was to guarantee that every option still standing was real, bookable, honestly priced, and actually met the constraints. Nine survivors out of a few hundred candidates. The final call took thirty seconds.
It would be dishonest to write this up as though the human just supervised. I went back through the logs — 59 turns — and the honest picture is that the four hard filters split into two very different kinds:
flowchart TD
C["Every constraint on the trip"] --> Q{"Could it be written down?"}
Q -->|"budget and dates"| SPEC["SPECIFIABLE — stated in the brief on day one — the agent's job is to REMEMBER it"]
Q -->|"'a good beach'"| DISC["UN-SPECIFIABLE — has no definition — must be DISCOVERED by counterexample"]
SPEC --> P["A harness you can trust needs BOTH: durable memory and counterexample elicitation"]
DISC --> P
The budget and the dates were in the brief from the first message. The two filters that ended up deciding everything — walk to soft, swimmable sand; no rocks — were not, and could not have been. "A good beach" isn't a spec; it's something you recognize. Those two got discovered the only way they could be: by rejecting a finished answer and reading what the rejection revealed.
Two honest complications, and neither is the user failing to specify. First: the budget tightened mid-project — from the brief's flexible "~$6–8k" to a hard $3,500 cap — and across a long, multi-session run the agent lost the thread on price. When the user caught it, the words were "you lost the price goals," which was exactly right: an agent had dropped a constraint it was handed. That's context rot, a real failure mode of long agent runs, and it's the machine's bug to fix. Second: the dates moved to August for a birthday, and in travel every price and availability check is keyed on dates — so each change silently rotted every number in the document. The trip legitimately changed; the system just didn't keep up.
Turn 44 of this project is one sentence long:
"The rocks are not good."
Before that sentence, "soft sand" was not a requirement. After it, it was the requirement — the one that ultimately decided everything, invalidated three prior workflows, and forced us to build a dataset that doesn't exist anywhere. The constraint did not exist, and then it did. A counterexample conjured it.
This is not a story about a user who failed to write good requirements. It's a fact about people, and once you take it seriously it changes how you build the tool.
Justice Potter Stewart's "I know it when I see it" (Jacobellis v. Ohio, 1964) gets quoted as a punchline about judicial laziness. It's actually an accurate report of how categorization works for concepts that resist definition by necessary-and-sufficient conditions — what Wittgenstein called family resemblance. "A good beach" has no definition. It has exemplars.
Decision researchers have the harder-nosed version. Paul Slovic's line of work on the construction of preference holds that people don't read values off a stable internal utility function; they build one at the moment of choice, and the construction depends on what they're shown and how it's framed. Ask someone to specify what they want and you get a lossy, partial reconstruction. Show them a violation and you get an instant, confident, correct rejection.
Which yields the asymmetry the whole harness should be designed around:
Recognition is cheap and reliable. Specification is expensive and lossy. Rejections are high-bandwidth.
Here's the part that should feel uncomfortable to anyone who's trained a model. The user could not write down their objective function. Neither could the agent. But the user could reliably label a candidate as bad, and say roughly why.
So what we ran, for 59 turns, was online preference learning from negatives. Each rejection was a comparison label. The "locked constraint table" was a reward model, fitted incrementally, badly, and mostly from things the user hated. RLHF exists for exactly the same reason "gather the requirements" fails in the first place: for the things that matter most, humans can evaluate but not specify. We built a whole industry on that asymmetry and then act surprised when it shows up in a condo search.
Stigler's 1961 The Economics of Information gives the boring, correct frame: optimal search depth is a function of the marginal cost of one more query. When each iteration costs you a weekend of browsing tabs, the rational play is to commit early to a spec you haven't tested — or to satisfice in Simon's sense and stop at "good enough, I'm tired."
Collapse the cost of an iteration to ten minutes and a few hundred thousand tokens, and the calculus inverts. "I'll know it when I see it" stops being an unaffordable luxury and becomes the efficient strategy.
Which also gives the fix. If people can't state constraints but can recognize violations, then stop asking and start showing. Before any expensive search, spend ~1% of the budget generating five deliberately diverse candidates — the cheapest thing that qualifies, the extravagant one, one that's perfect-but-for-a-flaw, one that's just weird — and ask a single question: "which of these are wrong, and what makes them wrong?"
That probe, run on turn 3, would have surfaced the rock allergy in minutes instead of on turn 44 — and it would have caught the budget tightening the moment it happened, instead of letting a long session quietly lose it. It's now Step 0 of the skill.
Cheap generation makes mis-specification more dangerous, not less. An agent will pursue a stated objective with superhuman efficiency and hand you a confidently wrong answer, beautifully typeset. We lived this: for about twenty turns the dossier was a gorgeous, sortable, thumbnail-rich, dark-mode-aware monument to a recommendation that was over budget and had no ocean view. Goodhart's law with better typography.
And the failure is self-reinforcing, because a polished artifact teaches you to want what it found. Anchoring plus the persuasive authority of good design. Left unchecked, the tool stops helping you discover your preferences and starts manufacturing them.
Verification is the only thing standing between those two outcomes. Which is why "verify before you decorate" is a moral instruction, not a style guide.
Once every hard constraint is satisfied, look at what's actually left. Nine survivors. Beachfront-on-Kamaʻole-I at $3,423, versus 1,000 square feet across the road from a better beach at $2,937. Which is correct?
Neither. There is no exchange rate between "steps onto sand" and "an extra room" and "$486." These are incommensurable goods, and any weighted score that pretends to rank them is just someone's opinion wearing a lab coat. (Arrow has a theorem about this mood.)
So the division of labor isn't a hedge about AI safety. It falls out of the structure of the problem:
Agents enforce facts. Humans adjudicate values.
Constraints are factual, verifiable, and therefore delegable — is it available, is it under $3,500, is the sand actually soft. Tradeoffs among satisfied constraints are not factual at all. 249 agent runs took a few hundred candidates down to nine. A human took nine down to one in about thirty seconds, and picked the roomy one next to the best water. Both numbers are correct. Neither party could have done the other's job.
In the spirit of saving everyone some time:
I could have gotten a worse answer in an hour. An hour of browsing surfaces neither the front-runner that was silently booked, nor the owner-direct link redirecting to a different building, nor a beach-quality ranking that exists in no database. I did spend the hour — on a version of the trip that later changed, chasing a "good beach" I couldn't define until an agent showed me a bad one.
Correct, and that's the interesting part. It's absurd at today's prices, for this stake. Both of those numbers move, and they move in opposite directions. The economics only work when the cost of being wrong is large and the commit is hard to undo. Which brings us to:
When P(wrong) × cost(wrong) > cost(verification). For a $3,000 booking with a birthday attached, verification is nearly free by comparison — verify aggressively. For a $30 reversible purchase, don't run any of this. Buy the socks.
Because the deciding attribute wasn't a filter. There is no beach_is_soft checkbox, and the only parties positioned to add one are the parties with an incentive to always check it.
Constantly, and always in the direction of optimism. Free text let them launder "a strip of ocean visible over the neighbor's roof" into "ocean view." The fix wasn't a better model — it was forcing them to pick one of four enum values and then paying a second model to attack the answer. Hallucination is a formatting problem more often than people admit.
Over 1.5M subagent tokens across the measured workflows, roughly 35 minutes of agent wall-clock for the five biggest, and less money than one night of the condo it booked.
Strip out Maui and this generalizes to any high-stakes, low-frequency, multi-constraint purchase — apartments, contractors, weddings, used cars, schools:
The recipe above isn't a blog-post abstraction — it's a file. Claude Code lets you write skills: markdown documents that get loaded into context when a matching task shows up. So the hard-won lessons don't evaporate when the session ends; the next search inherits them.
Two came out of this project:
/trip-planner — the domain workflow (fan-out research, live MCP pricing, all-in cost model, HTML dossier). It existed before Maui. It has now been amended with everything that went wrong: lock the dates before deep pricing, don't start until you have the ceiling and the hard filters, and a list of the traps that each cost a crowned pick./adversarial-verify — the general one, and the more interesting one. It has nothing to do with travel. It's the harness for any decision where the person writing the data benefits from your believing it: rentals, contractors, vendors, marketplaces, job posts.It isn't a manifesto; it's machinery. Nine files: the skill itself, four references, three runnable workflow templates, and a checklist you read out loud before the user clicks buy.
adversarial-verify/
├── SKILL.md # the harness
├── references/
│ ├── preference-construction.md # why constraints arrive late; design for it
│ ├── traps.md # the 200-OK lie, marketing laundering
│ ├── schemas.md # enum library; why free text is unsafe
│ └── stopping-rules.md # Δp·L > c — when to stop verifying
├── templates/
│ ├── verify.workflow.js # skeptics w/ diverse lenses; majority-refute
│ ├── prove-negative.workflow.js # adjacent-category sweep
│ └── artifact-audit.workflow.js # chunked contradiction sweep
└── checklists/pre-commit.md
Step 0 is the one I'd defend hardest, because it's the fix for the failure this whole post is about:
# Step 0 — Probe before you search
> The user cannot tell you their constraints. They can only recognize
> violations. So don't ask. Show.
Spend ~1% of budget on 5–7 deliberately diverse candidates — including
ones you expect to be wrong — and ask exactly this:
"Which of these are wrong, and what specifically makes them wrong?"
Then: filter in code, never in a prompt. Uncertainty fails.
Majority-refute kills. Ties go to the skeptics.
That's the artifact I'd actually hand a friend. Not the trip plan — the harness that produced it.
Aggregators solved finding. Nobody solved trusting. Every booking site on earth will show you a price for a room that's unavailable, a beach that's rocky, and a "verified" owner link that redirects to another property. The listings are an adversarial data source, and the incumbents' incentives are not aligned with telling you so.
The defensible layer isn't the index — it's the verification harness: the browser fleet that confirms a Reserve button is actually live, the skeptic agents that demand photo evidence for an "ocean view," the constructed columns for qualities no schema captures, and a receipt trail showing exactly how each claim was checked.
It's expensive per query — 249 agent runs and well over a million tokens for one booking. Which is precisely why it works for decisions where being wrong costs $3,000 and a ruined birthday, and doesn't work for buying socks. That's a real market, and it's the one where "AI search" currently fails the hardest.
The trip is booked. The beach is soft. The rest is a product idea.