Inspiration
Anyone who has organized food for a club meeting, a hackathon or a department event in Los Angeles knows the routine: call five caterers, wait for three to answer, compare quotes that list different things, ask each one for a better price, chase a deposit, and then watch trays of good food go in the trash at the end of the night.
None of those steps is hard. They're just slow, repetitive back-and-forth, which is exactly what agents are good at. We wanted to see whether a group of agents could do the whole job inside one chat: find real caterers, get real quotes, haggle within each business's rules, take the deposit, book, and then make sure the leftovers get eaten instead of thrown away.
What it does
You tell @haggle in ASI:One about your event: date, time, place, number of guests, budget, dietary needs and allergies. Haggle then:
- Finds caterers near the venue through Google Places and Agentverse search, and shows them as cards with photos and a Connected / Not connected badge.
- Gives every caterer an agent. Businesses that set up ("claimed") their Haggle agent negotiate live. For the rest, our Vendor Agent Factory reads the public menu (web pages, PDFs, even photos of menus via OCR) and builds an unofficial agent that can only give estimates.
- Collects quotes in parallel and ranks them by fit, budget and rating.
- Haggles. Each tap on Negotiate lower runs several rounds of ask and counter-offer between our Negotiator and the caterer's agent, shown as a transcript. If the ask goes past what the caterer allows on its own, the owner gets an iMessage and can reply YES, NO or a price.
- Books through Fetch.ai's Payment Protocol with Stripe. The Booker checks the deposit with Stripe itself, confirms with the caterer, and sends a confirmation email with a calendar invite, the order to the caterer, and an iMessage.
- Follows up. It sends a reminder the day before. After the event ("event over"), it asks about leftovers and matches them to LA food banks and pantries by food type, food safety, pickup or drop-off, opening hours and distance. Then it sends the rescue request and shows how many meals were saved.
How we built it
Haggle is 6 Haggle agents plus one agent per caterer, all built with Fetch.ai uAgents in mailbox mode and registered on Agentverse:
| Agent | Job |
|---|---|
Concierge @haggle |
ASI:One chat, intake checklist, Interactive Cards, session state, Payment Protocol (seller) |
| Discovery | Google Places + Agentverse search for caterers near the venue |
| Vendor Agent Factory | Reads menus and creates vendor agents |
| Negotiator | Parallel quote requests, counter-offer rounds, ranking |
| Booker | Stripe verification, caterer confirmation, email, calendar invite, iMessage |
| Follow-up | Reminders, post-event check-in, food rescue |
| Vendor agents | One per business; claimed (negotiates) or unclaimed (estimates only) |
The agents talk through shared message models (RFQ, Quote, Counter, Accept, BookingConfirmed, LeftoverReport, …), and every action is idempotent on the request id, so a repeated message can never double-book or double-charge.
Other tools and services we used:
- ASI:One LLM (
asi1) reads free text and menus and writes friendly notes. That's all it does. - Two public Agentverse agents as tools: a Tavily search agent finds menu pages, and an Image Generator agent made photos for our demo caterers. Both have timeouts and fallbacks.
- Stripe (test mode) for deposits, Resend for email with
.icsinvites, and Photon for iMessage, including owner replies read back from the inbox. - Sentry for tracing.
All money math lives in code, never in the LLM
Price. Each dietary group is priced at the average per-person price of the menu items that fit it. Side items don't drag the average down: an item counts as a meal only if it costs at least half of the most expensive main, \( p_i \ge 0.5 \, p_{\max} \).
How far a caterer will go. The owner sets a maximum discount \( d_{\text{owner}} \). A caterer gives more room on big orders and none on small ones:
| Order total \( L \) | Discount room \( d(L) \) |
|---|---|
| under $400 | 0 (no haggling on small orders) |
| $400 – $800 | \( 0.5 \cdot d_{\text{owner}} \) |
| $800 – $1,500 | \( 0.8 \cdot d_{\text{owner}} \) |
| $1,500 and up | \( d_{\text{owner}} \) |
The floor, the lowest price the caterer accepts without asking its owner, is:
$$ F = L \left( 1 - d(L) \right) $$
Haggle's side. Haggle opens with a target \( T_1 \): the budget \( B \) if the quote is over it, otherwise a discount that grows with the order (6% under $800, 10% under $1,500, 15% above). It then meets the caterer halfway:
$$ T_2 = \frac{O_1 + T_1}{2} $$
where \( O_1 \) is the caterer's first counter-offer. On each round, the caterer:
- accepts if the ask is at or above its floor (\( T \ge F \));
- in round 1, counters halfway, never below the floor: \( \max\left( F, \frac{\text{last} + T}{2} \right) \);
- in round 2, offers its floor \( F \) as its best price;
- in round 3, sends anything below the floor to the owner by iMessage.
Deposit: \( D = 0.20 \times \text{total} \), paid through the Payment Protocol and then checked against Stripe for the right request and the right amount.
Food rescue. Distance from the venue to each food bank uses the haversine formula, with Earth's radius \( R \approx 3958.8 \) miles:
$$ d = 2R \arcsin \sqrt{ \sin^2 \left( \frac{\Delta \varphi}{2} \right) + \cos \varphi_1 \cos \varphi_2 \sin^2 \left( \frac{\Delta \lambda}{2} \right) } $$
The impact is an estimate, labeled as one: \( \text{meals} \approx \text{lb} / 1.2 \), where a tray is about 24 lb, or roughly 20 meals.
Challenges we ran into
- Menus without prices. Many caterers publish their menu as a PDF, a photo or a "call us" page. The Factory follows menu and catering links, reads PDFs, runs OCR through a vision model, and only prices what it can actually read. Caterers with no public prices get a Price on request card and an "Email them for a quote" button. We never invent a price.
- LLM latency. Our first menu extraction took 117 seconds because the model reasoned at length. Setting
reasoning_effort="minimal"brought it down to about 5 seconds. - ASI:One drops messages from other conversations. Each chat has a conversation ID, and ASI:One silently ignores messages that don't carry it. Anything sent later (an owner's YES, a Stripe payment we noticed in the background, a reminder) disappeared until we made every update reuse the conversation ID of the user's latest message.
- Payment UX. ASI:One's Confirm Payment only opens Stripe's card form for an embedded Stripe checkout. A Stripe-hosted page did nothing when clicked. We also watch Stripe directly, so a paid deposit books even if the payment message never arrives.
- Two assistants in one chat. ASI:One's own assistant sometimes replied to our cards ("Good outcome, $500 vs $742.60!"), and Haggle read that as the user changing the event. Once quotes exist, only "start over" can change the request now.
- An unreliable network. At the venue we hit Wi-Fi drops, Agentverse mailbox errors, duplicate deliveries and late replies. We added longer waits, ignoring of repeated replies, adding late quotes to the carousel, and resending a lost request after 45 seconds.
- Quotas and limits. We filled our Agentverse mailbox slots, and every restart wiped the search cache, so we used up Google Places' daily Text Search quota mid-demo. Resend's test sender only delivers to one inbox, and Photon requires idempotency keys on every call. Each one took a fix.
- ASI:One card quirks. Long labels squeeze values into one character per line, single line breaks get merged, and row layouts stack vertically. We changed our card design until they rendered cleanly.
What we learned
- The LLM reads; the code decides. Keeping prices, floors and payments in deterministic code made negotiation predictable, testable (26 unit tests) and safe to demo.
- Assume every message can be lost, delayed or delivered twice. Making every action idempotent on the request id was what made retries, replays and slow networks harmless.
- The chat interface is part of the protocol. Conversation IDs, card rendering and how ASI:One's assistant behaves mattered as much as our own agents' logic.
- Agent-to-agent negotiation feels real when it has rules. A caterer that gives more room on big orders, says no to tiny ones, and checks with its owner past a limit behaves like an actual business, and lets the user see exactly why the price moved.
- Composing with other agents is powerful but needs guard rails. Public Agentverse agents saved us hours, and we learned to give every such call a timeout, a fallback and a log line.
Built With
- agentverse
- asi-1
- asi:one
- chat-protocol
- fetch.ai
- google-places
- imessage
- interactive-cards
- multi-agent-systems
- openai-sdk
- payment-protocol
- photon
- pydantic
- pypdfium2
- python
- resend
- sentry
- stripe
- tavily
- uagents
Log in or sign up for Devpost to join the conversation.