Build a payment reminder agent that never chases a paid customer
I build Settl, an expense-splitting app, so I am careful about letting AI near money state. The model can write a polite reminder. It should never decide whether a person owes money. This pack keeps those two jobs separate.
Why reminder agents go wrong
A spreadsheet says overdue. The payment provider says paid. Someone sends a reminder from the spreadsheet anyway, and a good customer receives a message that makes your operation look careless.
Choose one source of truth for payment status. That is the record your system trusts when two places disagree. It may be your payment provider or an internal ledger that is updated from verified payment events.
Stripe, for example, sends payment and dispute events to registered webhook endpoints. Its current guidance also requires signature verification before trusting those events. Read the official webhook documentation for the exact integration you use.
Keep eligibility deterministic
Write reminder eligibility as code or a rules table. Do not ask the model, "Should we remind this customer?"
{
"invoice_id": "inv_demo_1042",
"status": "overdue",
"due_at": "2026-09-01T00:00:00Z",
"amount_cents": 48000,
"currency": "SGD",
"reminder_count": 1,
"last_reminded_at": "2026-09-03T02:00:00Z",
"disputed": false,
"paused": false
}
The eligible set should be boring:
status is overdue
AND disputed is false
AND paused is false
AND amount_cents is greater than zero
AND enough time passed since the last reminder
AND reminder_count is below the limit
Paid, void, disputed, refunded, and paused records stop before any model call.
Make the source-of-truth rule explicit during setup. If the provider and your invoice table can disagree, document which one wins and how the losing record is repaired. A reminder job is not the place to reconcile money state. It should stop, raise an internal task, and wait for a trustworthy status.
Freshness matters as much as status. Record when the payment state was last verified. If updates have been failing or the record is older than your safe window, pause the reminder. Sending nothing for an hour is less damaging than accusing a paid customer.
The writing prompt
Once code decides the invoice is eligible, the model can choose wording inside a narrow policy.
Write one payment reminder using only INVOICE_FACTS and CUSTOMER_FACTS.
Rules:
- State the invoice number, amount, currency, and due date exactly as supplied.
- Do not threaten, shame, invent fees, or claim legal consequences.
- Do not say the invoice is unpaid if status is not overdue.
- Use the tone assigned to this reminder number.
- Include the supplied payment link and reply option.
- Keep the message under 90 words.
Return JSON with subject, body, and facts_used.
Keep tone bands explicit. Reminder one can be a neutral check. Reminder two can be direct. Anything after your chosen limit should become a human task, not an angrier model prompt.
The tone policy should cover what the model must leave out. Do not manufacture urgency, threaten account closure, or mention fees that are not already part of the invoice. Give every message a plain reply path for customers who paid another way or believe the invoice is wrong. Those replies should pause automation immediately.
Recheck before sending
Time passes between selecting invoices and sending messages. Re-read the current payment status immediately before delivery.
1. Select an eligible invoice.
2. Generate a draft.
3. Validate every amount, date, link, and invoice id.
4. Re-fetch current status.
5. Stop if paid, disputed, void, refunded, or paused.
6. Record an outbound-message id.
7. Send once.
8. Save the delivery result.
That second status check prevents the classic race: payment arrives at 9:01 and the queued reminder leaves at 9:02.
Keep an audit trail
Store what the system knew when it sent the reminder. You need enough detail to answer a customer who says the message was wrong.
{
"message_id": "pay_reminder_inv_demo_1042_2",
"invoice_id": "inv_demo_1042",
"status_checked_at": "2026-09-05T01:02:00Z",
"status_seen": "overdue",
"template_version": "payment-reminder-v3",
"approved_by": "rules-engine",
"delivery_status": "sent"
}
Build the message id from stable inputs so a retry targets the same record. A repeated job should not send a second message.
Keep the chosen invoice facts and the final sent copy together. Do not rely on recreating them later from the current invoice because the amount, owner, or due date may have changed. Your audit record should explain what the system saw, which rule made the invoice eligible, and what stopped or allowed delivery.
Test the cases that damage trust
1. Payment arrives after selection but before send.
Expected: final status check stops the reminder.
2. Invoice is disputed.
Expected: no draft and a human task.
3. The same job runs twice.
Expected: one outbound message id and one send.
4. Model changes the amount or due date.
Expected: validation rejects the draft.
5. Payment event has an invalid signature.
Expected: status does not change.
Use test-mode invoices and addresses until the audit records match every expected branch.
Add a dry-run report before enabling delivery. It should list the invoices that would receive a message, the ones that were blocked, and the reason for every decision. Review that report with someone who understands your billing process. A clean sample is stronger evidence than a clever prompt.
Once live, watch the outcomes that signal trust damage: replies saying "already paid," disputes opened soon after a reminder, duplicate deliveries, and manual pauses. Do not judge the agent only by money collected. A system that collects quickly while annoying reliable customers is still a bad system.
Run this on your codebase
Inspect this project and design a payment reminder agent with deterministic eligibility.
1. Find the payment provider integration, invoice table, webhook handler, job queue, and mail code.
2. Do not edit anything yet. Identify the current source of truth for payment status.
3. Define explicit statuses that block reminders: paid, void, disputed, refunded, and paused.
4. Write eligibility rules in code or data, never in the model prompt.
5. Verify provider events before updating payment state.
6. Draft messages only after an invoice passes the rules.
7. Validate invoice id, amount, currency, due date, and link against stored data.
8. Re-fetch payment status immediately before sending.
9. Make retries reuse one stable outbound-message id.
10. Add tests for late payment, disputes, duplicates, altered facts, and failed delivery.
Return the state model, rule function, prompt, audit record, file plan, and tests before coding.
End with the smallest safe test-mode slice.
Download the runnable pack
This is a complete starter for the workflow in this guide, not a screenshot. The safe default validates fictional input and returns a deterministic, reviewable draft. It does not publish content, message a customer, change pricing, edit production data, or execute another external action.
- Download the complete pack
- Import the n8n workflow
- Open the sample input
- See the expected dry-run result
- Run with Docker Compose
- GitHub validation workflow
- GitHub manual run workflow
Run the downloaded folder locally:
npm test
npm run validate
npm run sample
cp .env.example .env
docker compose up --build
The ZIP includes an importable n8n webhook, a dependency-free Node 22 service, Docker Compose, GitHub Actions validation and manual-run workflows, fictional sample input, expected dry-run output, and tests. If you later enable Claude, keep the API key in the service environment. The n8n export contains no credentials. Live mode still returns a draft for approval.