DEV Community

Kirthi Sagar
Kirthi Sagar

Posted on

How I Built an AI Dunning Agent That Recovers Failed Payments on Autopilot

I built Dunning for the All Things Agentic Hackathon. This post covers why I made it, how it works under the hood, and the mistakes I made along the way. I am publishing this piece of content for the purpose of entering the All Things Agentic Hackathon on Devpost.

The Problem Nobody Talks About

If you run a SaaS business, you lose money to failed payments every single month. A credit card expires. A bank declines a charge. The customer never even knows it happened, and you just lost recurring revenue.

The industry calls this involuntary churn, and most companies either ignore it or send a couple of generic retry emails that get buried in inboxes. Collections agencies charge 25-30% of what they recover. That felt broken to me.

What Dunning Actually Does

Dunning is a full-stack web application with an AI agent at its core. When a payment fails, the agent takes over. It figures out who the debtor is, looks at their timezone and jurisdiction, picks the right tone and legal language, and sends a sequence of recovery emails. If the debtor responds, the agent negotiates directly - it can offer payment plans, small discounts, or just answer questions about the invoice.

The whole thing runs without human intervention. You connect your billing platform, set your rules, and the agent handles the rest.

You can see it live at dunning.website.

The Tech Stack

Frontend: Next.js 14, deployed on Vercel. Server components, a dashboard for managing invoices and rules, a billing page where customers can pick their subscription tier.

Backend: Python/FastAPI, deployed on Google Cloud Run. This handles authentication (Clerk), the REST API, webhook processing, and orchestrates the AI agent.

AI Agent: This is the heart of it. The negotiator agent uses Gemini through Google Cloud. It has access to tools like generate_recovery_link (creates a real checkout page via DodoPayments) and send_email (sends through Resend). The agent decides when to escalate tone, when to offer a discount, and when to stop emailing entirely.

Database: PostgreSQL on Cloud SQL. Stores invoices, dunning rules, organization policies, success fees, and the full conversation thread between the agent and each debtor.

Payments: DodoPayments for subscription billing and checkout link generation. Each recovery email includes a unique payment link that the debtor can click to pay immediately.

How the Agent Works

The agent is not a chatbot. It runs in the background on a cron schedule. Every cycle, it:

  1. Queries the database for invoices in an active dunning state
  2. Checks the dunning rules to see what action comes next
  3. Generates a personalized email using Gemini, adjusting the tone based on how many days past due the invoice is
  4. Creates a one-click payment link through DodoPayments
  5. Sends the email via Resend
  6. Updates the invoice state in the database

If the debtor replies to any of these emails, the response comes in through a Resend webhook. The agent reads the reply, generates a response, and continues the conversation. It can negotiate - if the debtor asks for a payment plan, the agent evaluates whether the organization's rules allow it and responds accordingly.

Mistakes I Made (And Fixed)

Database schema drift. I added new columns to the SQLAlchemy model (jurisdiction, debtor_country, debtor_timezone) but forgot to run the Alembic migration on the production database. The invoices page returned a 500 error for hours before I tracked it down to a missing column. The fix was a manual ALTER TABLE against the Cloud SQL instance.

Hardcoded API URLs. My billing portal endpoint was manually constructing URLs to the DodoPayments live API instead of using the Python SDK. When I switched from the live API key to the test key, those hardcoded URLs broke with a 401. I replaced every requests.post call with the official SDK methods, which respect the environment setting automatically.

Secret overwriting. My GitHub Actions workflow used gcloud run deploy --set-secrets which replaces all secrets on the service. One deploy wiped out the database URL, Clerk keys, and everything else. I changed it to --update-secrets which only adds or updates the ones you specify.

What I Would Do Differently

The agent currently creates a new DodoPayments product for every single recovery email. That works, but it clutters the product catalog. A better approach would be to create products once and reuse them.

I would also add a proper queue (Cloud Tasks or Pub/Sub) instead of running the agent on a cron timer. Right now, if the cron job takes too long, it could overlap with the next one.

Try It

The project is live at dunning.website. If you run a SaaS and failed payments are quietly eating your revenue, give it a look.


Built for the All Things Agentic Hackathon using Gemini, Google Cloud Run, and the Agent Development Kit.

Top comments (0)