← Back to work

Full-stack engineer (solo build) · 2026

LedgerNudge

An AI-native B2B invoice follow-up tool: Claude drafts respectful payment reminders, a human approves them, Stripe takes payment, and Claude reads each reply to decide whether to keep going or pause for a dispute.

Chases unpaid invoices on its own, but a person signs off every message before it sends.

Source ↗
Cover image for LedgerNudge

The problem

Chasing unpaid B2B invoices (dunning) is mostly manual, repetitive, and easy to get wrong: send too softly and nobody pays, send too aggressively and you damage the relationship. I wanted to see how much of that follow-up loop could become software where a person still controls anything that leaves the building, and a language model handles the parts it is genuinely good at: writing the message and reading the reply. I also set myself a constraint: build it solo in Laravel and PHP, a stack I had not written before, to prove I can ramp fast on a new language with AI directing the work. LedgerNudge is a deliberately scoped slice of that idea, presented honestly as a portfolio project rather than a product.

Approach

The whole design is human-in-the-loop. Claude drafts each reminder from the invoice, the debtor's history, and a per-account tone policy; an operator approves or edits the draft before it is ever sent; approved messages go out over a queue. Stripe takes the payment through a hosted link, and a webhook reconciles the result back onto the invoice. Inbound replies feed back in: Claude classifies each one, and a dispute (or anything the model is not confident about) pauses the sequence and flags a human. The bias is deliberately toward pausing: when in doubt, stop and ask a person.

Architecture

The dunning loop: draft, approve, send, reconcile, classify, pause.
The dunning loop: draft, approve, send, reconcile, classify, pause.

How I built it

I delivered it in SCRUM-style sprints, each one leaving the project working and tested and landing as a single commit, so the git history reads like a delivery log. Every push runs CI (the test suite plus linters for PHP and the frontend). Because I had never written PHP or Laravel before, I used Claude Code to direct the work and learned the framework by shipping it, one reviewable slice at a time.

What I'd do differently

The thin adapters that wrap the real Stripe, Claude, and Twilio SDKs are not unit-tested, because that needs live credentials; the seams around them are, which is where the logic actually lives. The email inbound route trusts its payload as a simplified stand-in for verifying a mail provider's signature. Voice follow-ups, multi-tenant brand isolation beyond the tone-policy field, and a real jurisdiction-aware compliance rules engine are named in the README as the next layers, not built.