# I Built an AI Agent That Negotiates My Bills For Me, Here's Everything That Went Wrong

> Source: <https://dev.to/abdullahinnit/i-built-an-ai-agent-that-negotiates-my-bills-for-me-heres-everything-that-went-wrong-560l>
> Published: 2026-08-30 12:02:19+00:00

This post showcases my journey of the **All Things Agentic Hackathon**.

I'm a second semester software engineering student, and a few weeks ago I had an idea I couldn't stop thinking about, what if an AI agent could actually haggle on my behalf, the way I never have the nerve to on a retention call?

That idea became Haggle, an agent that researches real competitor pricing, then negotiates round by round against a second AI playing a subscription service's retention department, until it lands a deal or walks away. Built on Gemini 3.5 Flash, Google ADK 2.0, and the A2A (Agent-to-Agent) protocol, for the All Things Agentic Hackathon's **Taskmaster track**.

The idea took an afternoon. Getting it actually working took a lot longer, and almost none of the hard parts were about AI. Here's what actually happened.

Here's the architecture in brief:

There are two independent agents, not one prompt pretending to be two people:

*UserAgent*: takes a goal (service, current price, target, walk-away ceiling), researches real competitor prices via Google Search grounding, and negotiates

*CounterpartyAgent*: runs as its own standalone A2A server, plays a retention specialist with a hidden floor price it will never reveal

They talk to each other over real HTTP, using Google's A2A protocol, the same standard designed for agents built by different systems to negotiate with each other. It felt right for a project literally about negotiation.

**Bug #1**: Gemini won't let you mix a built-in tool with a custom one

The first real wall: giving the UserAgent both google_search (a built-in ADK tool) and a custom format_research function tool on the same agent. Gemini flatly refuses to mix a built-in tool with function calling in one agent, you get a 400 INVALID_ARGUMENT the moment it tries. The fix was architectural, not a patch, split search into its own dedicated ResearchAgent, holding only google_search, wrapped as an AgentTool the UserAgent calls instead. Cleaner design anyway, in hindsight.

**Bug #2**: A single character broke every JSON response

Both agents respond in structured JSON, and their instructions include a JSON example so the model knows the exact shape to return. One instruction was built with .format() (needs doubled braces {{ }} to escape literal braces), the other with .replace() (doesn't touch braces at all). I'd copied the doubled-brace JSON example into the .replace()-based prompt without adjusting it so the model was being shown literal {{...}} as "correct JSON" and dutifully mimicked it in every response, breaking my parser on every single round. A one-character-class bug that took a full review pass to actually catch.

**Bug #3**: The agent card was lying about its own address

This one was the strangest. The CounterpartyAgent's A2A server passed its readiness check every single time but the first real negotiation request always failed with a flat "all connection attempts failed." Turns out A2A agent cards are self-describing, they embed their own canonical URL for other agents to actually use. My server was calling to_a2a() with no explicit host/port, while uvicorn was bound separately so the card was advertising the wrong address entirely. The readiness check used one URL (hardcoded, correct); the real negotiation traffic used a completely different one (from the card, wrong). Consistent failure, two different code paths, easy to chase the wrong one for a while.

**Bug #4**: An emoji crashed the server, but only sometimes

print("🏢 Server starting...") completely fine when run directly in a terminal. Completely fatal the moment its output got redirected anywhere else (a log file, a subprocess pipe), because Windows silently falls back to an encoding that has no emoji in it at all. The server was crashing before it ever opened its port, and because I'd redirected its output to hide noise, I couldn't see why for a while. Lesson: if a subprocess's behavior changes based on how you're watching it, that's the tell.

**Bug #5**: The cloud deployment had its own entirely new rules

Getting this onto Google Cloud (Cloud Run Jobs + Vertex AI + Firestore) surfaced a fresh batch of issues nothing local had warned me about:

Newer Gemini models aren't always reachable at a specific region like us-central1, some are Vertex AI global endpoint only. A regional pin that worked for other models threw a flat 404 for this one.

A multi-line gcloud command using bash-style backslash continuation, pasted into PowerShell, silently mangled three separate environment variables into one garbled string. Vertex AI auth failed with a completely unrelated looking error as a result.

And the very first real blocker: Vertex AI requires a billing account before it'll respond to anything, even usage entirely covered by free credit and getting a billing-eligible card as a student with no family card in reach turned into its own multi day side quest.

None of these are AI problems. They're the unglamorous 80% of actually shipping something on real infrastructure, and I don't think that part gets talked about enough in hackathon writeups.

Where it landed

Multi-service batch mode:- hand it a list of subscriptions, it negotiates all of them autonomously and reports total savings

Every negotiation logged to Firestore, with a live public dashboard showing cumulative savings across every run

Deployed as a Cloud Run Job, authenticated via Vertex AI Application Default Credentials

A real test suite:- 15 tests, no API key needed, covering the exact bugs above so they can't quietly come back

A real run, verbatim: three services negotiated back to back, $16.00/month saved, $192/year, all three landing a deal — Netflix, Spotify, and Disney+, each negotiation genuinely different in what leverage it cited and how far it moved, because it's the model reasoning fresh every round, not a script.

*What I'd tell past me*

Most of the hard parts weren't "make the AI smarter." They were: read the actual error instead of guessing, check what's actually different between the working case and the broken one, and don't trust that a fix worked just because the summary of it sounds right, verify. That's a less exciting lesson than "I built an autonomous agent," but it's the one that got this project across the finish line.

Haggle was built for the All Things Agentic Hackathon, Taskmaster track. Code on [GitHub](https://github.com/abdullah-innit/haggle), live dashboard [here](https://haggle-dashboard-535623739933.us-central1.run.app).
