Closeout — Your Money Has a Memory Problem. Now So Does Your Agent.
Inspiration
The average household is bleeding $200+ a month on subscriptions they forgot they signed up for — a free trial that quietly converted, a streaming service nobody's watched since a road trip eighteen months ago, two duplicate cloud storage plans because nobody remembered cancelling the first one. Survey after survey finds the same thing: people underestimate their actual subscription spend by more than 2x.
This isn't a discipline problem. It's a memory problem. Banks show you a list of charges, not a story. "NFLX DIGITAL $17.99" tells you nothing about whether that's still the price you signed up for, whether you're paying for it twice under two different account names, or whether a free trial from your last vacation just quietly started billing you.
We wanted to build something that doesn't just show you this — it remembers everything for you, reasons about it, and acts. Not a budgeting app that dumps a spreadsheet on you. An agent that behaves like a sharp friend who reads your statements line by line, says "wait, you're paying Squarespace three different ways and Netflix raised your price 16% in March — want me to email them?" — and then actually drafts and sends that email when you say yes.
MongoDB's $rankFusion operator — landing as one of the first production-ready hybrid search primitives in any document database — was the spark. Bank descriptors are notoriously messy (SQSP* INC, SQUARESPACE.COM, Sqsp 8884). If we could fuse full-text search with vector similarity in a single aggregation stage, we could turn that noise into a clean, canonical subscription ledger — and build the agent's entire memory on top of it in one database.
What it does
Closeout connects to your bank transactions (via Plaid, sandbox for the demo) and turns raw noise into action:
- Detects recurring subscriptions — groups transactions by merchant, computes cadence (monthly/annual) and variance, entirely via MongoDB aggregation
- Flags the money leaks — price hikes (Netflix quietly went from $15.49 → $17.99), forgotten subscriptions (charged twice in 8 months and never used), duplicate services (Hulu and Netflix and two note-taking apps)
- Resolves messy merchant names —
$rankFusionfuses Atlas$search(BM25 text) with$vectorSearch(Voyage AI embeddings) to map garbage descriptors to real, canonical services with real support emails - Drafts real cancellation emails — the Gemini-powered agent writes a professional cancellation request, you review and edit it, and on approval the Gmail API sends it for real — message ID shown on screen
- Sets calendar reminders — before a free trial converts, the agent proposes a Google Calendar event; on approval it's created via the Calendar API
- Streams live alerts — MongoDB Change Streams push new transactions to the browser via Server-Sent Events the instant they're written, surfacing toast notifications with one-click cancel
- Forecasts your next 12 months — predicts upcoming charges per subscription and ranks them by urgency (a renewal in 3 days is "critical")
- Tracks every dollar saved — a
savings_ledgercollection records every confirmed cancellation, shown as a running total banner - Atlas Search autocomplete — fuzzy merchant search across your transaction history, type "netfl" and find Netflix instantly
- Atlas Charts dashboards — embedded analytics for spend trends, category breakdown, and agent action outcomes
Every action the agent takes is a proposal — nothing is sent or scheduled without explicit one-click human approval.
How we built it
Agent brain — Google ADK + Gemini on Vertex AI. The core is a google.adk.agents.LlmAgent running Gemini, with a perceive → decide → propose → remember loop defined in its system prompt. It's given two kinds of tools:
- The MongoDB MCP Server, registered as an ADK
MCPToolset(launched as a managednpx mongodb-mcp-serversubprocess) — the agent's raw read/write access to its own memory - Custom
FunctionTools wrapping pre-built aggregation pipelines — recurrence detection,$rankFusionmerchant resolution, price-hike analysis, portfolio summaries, and proposal-writing tools
MongoDB Atlas as the single source of truth. One cluster, no fragmented stack:
transactions— raw + enriched bank data, with a vector index on Voyage AI embeddings of merchant descriptors and a text search index for$searchservices_catalog— canonical service directory (support emails, cancellation URLs, billing descriptor variants) — this is the$rankFusiontargetactions_log— every agent decision, proposal, approval, and outcome — the agent's auditable long-term memorysavings_ledger— confirmed cancellations and amounts saved- Change Streams on
transactionspower the real-time SSE pipeline; an ObjectId-polling fallback keeps the same UX alive on M0 free-tier clusters that don't support Change Streams
Frontend — Next.js 14 (App Router) + TypeScript + Tailwind, deployed as a six-tab dashboard: Subscriptions, Forecast, Actions, Analytics, Calendar, and an AI Chat that talks directly to the ADK agent.
Backend — FastAPI on Cloud Run, exposing REST endpoints for ingestion, analysis, OAuth, analytics, forecasting, search, and the SSE event stream, plus the ADK Runner for the conversational agent.
CI/CD — Cloud Build builds both Docker images, deploys both Cloud Run services, captures the agent's URL, bakes it into the web build, and wires CORS between them — all from a single gcloud builds submit.
Challenges we ran into
$rankFusionis brand new — documentation and examples were sparse. We had to hand-build the hybrid pipeline combining$searchand$vectorSearchresult sets with rank-based score fusion, then validate it against deliberately messy seed data (SQSP* INC→Squarespace).- MCP server needs Node.js, but our agent container is Python — the MongoDB MCP Server runs via
npx, which silently isn't available in apython:3.11-slimimage. The agent would build and deploy successfully, then fail the moment it tried to use its database memory. We caught this in a final pre-submission audit and added a Node.js layer to the Dockerfile. - Real OAuth credentials, not just tokens —
googleapiclient.build()needs an actualgoogle.oauth2.credentials.Credentialsobject, not a plain token string. Early on, "Approve & Send" silently failed with a generic "Failed to fetch" until we traced it to this type mismatch. - M0 free tier doesn't support Change Streams — rather than requiring users to pay for M10+, we built a
_detect_change_streams()probe that falls back to 5-second ObjectId-ordered polling, feeding the sameasyncio.Queueand the same SSE generator either way. The frontend shows a green "Live · Change Stream" or yellow "Live · polling" dot — same UX, different engine underneath. - Cloud Build substitution syntax — local shell variables (
$AGENT_URL, computed mid-pipeline to wire the web build to the freshly-deployed agent URL) collided with Cloud Build's own$VARsubstitution syntax, and a${PROJECT_ID}reference inside a substitutions default doesn't expand the way you'd expect. Both required careful$$-escaping and hardcoding to get the agent → web → CORS handoff working in one pipeline run. - Atlas Charts SDK vs. Next.js — the official
@mongodb-js/charts-embed-domSDK pulls inbson, which uses top-levelawaitand broke the Next.js 14 webpack build entirely. We dropped the SDK and embed Atlas Charts via plain<iframe>— simpler, zero extra dependencies, and just as live.
Accomplishments that we're proud of
- A genuinely agentic flow — Gemini reasons over real aggregation output and writes a specific, correct cancellation email referencing the actual service, not a templated form letter
- One database doing everything: vector search, full-text search, hybrid
$rankFusionranking, real-time Change Streams, aggregation-based forecasting, and the agent's own action memory — all in MongoDB Atlas - The agent's memory and tools flow through the MongoDB MCP Server, not bespoke driver calls — a clean, reusable agentic integration pattern
- A "propose, never auto-execute" safety model that still ends in a real, live Gmail send and a real Calendar event — judges see an actual message ID, not a mock
- A resilient real-time layer that works identically whether you're on a $0 M0 cluster or a production M10+ replica set
- Shipped a fully working six-feature product (detection, hybrid resolution, forecasting, live alerts, autocomplete, analytics) end-to-end on Cloud Run with one-command CI/CD
What we learned
$rankFusionis a genuinely new way to think about search relevance — instead of choosing between lexical and semantic search, you can score-fuse both and let each cover the other's blind spots- ADK's
MCPToolsetmakes "give my agent a database" almost embarrassingly simple — but the runtime environment (Node.js fornpx-launched MCP servers) needs the same care as any other production dependency - Designing for the free tier (M0, no Change Streams) from day one made the architecture more robust, not less — the polling fallback turned into a feature (graceful degradation visible to the user) rather than a workaround
- Small infrastructure details — a missing
min-h-0in a flex layout, a$$in a YAML heredoc — can block an otherwise-complete feature, and a final "audit pass" before submission is as important as building the feature itself
What's next for Closeout
- Negotiation mode — beyond cancelling, have the agent draft "please match the lower price I'm seeing elsewhere" retention emails
- Multi-account support — connect multiple bank accounts and emails, with per-user MongoDB scoping
- Production Plaid + real banks — move from sandbox to Plaid production with proper account linking
- Expanded services catalog — community-contributed cancellation instructions and support contacts, searchable via the same
$rankFusionpipeline - Proactive digest emails — weekly "here's what changed in your subscriptions" summary, generated by the agent and sent automatically (with opt-in)
- Mobile app — push notifications powered by the same Change Streams pipeline
Built With
- atlas-charts
- atlas-search
- change-streams
- cloudbuild
- cloudrun
- docker
- fastapi
- gemini
- gmail-api
- google-adk
- google-calendar-api
- mongodb-atlas
- mongodb-mcp-server
- nextjs
- oauth2
- plaid
- python
- rankfusion
- tailwindcss
- typescript
- vectorsearch
- vertexai
- voyageai
Log in or sign up for Devpost to join the conversation.