Using an LLM to Explain Failed Payments to Customers
Pattern: 1) Get gateway_response from Paystack (from GET /transaction/verify/:reference or charge.failed webhook). 2) Send gateway_response + channel (card/mobile money/bank transfer) to LLM. 3) Prompt: "Translate this payment failure message into a friendly, actionable explanation for a customer. Tell them what to do next. Do not reveal internal error codes or system details." 4) Return the LLM response to the customer. Always include a next step: try a different card, check balance, contact bank, or try again later. Never show the raw gateway_response to customers.
Payment Failure Explanation Generator
// Cache for common failure explanations (same input → same output)
var explanationCache = new Map();
async function explainPaymentFailure({ gateway_response, channel, amount_ngn }) {
var cacheKey = gateway_response + ':' + channel;
if (explanationCache.has(cacheKey)) {
return explanationCache.get(cacheKey);
}
var channelContext = {
card: 'This was a card payment.',
mobile_money: 'This was a mobile money payment (M-Pesa/Airtel Money).',
bank: 'This was a bank transfer payment.',
ussd: 'This was a USSD payment.',
}[channel] || '';
var prompt = `A customer's payment of NGN ${amount_ngn} failed with this bank message: "${gateway_response}". ${channelContext}
Write a brief, friendly explanation for the customer that:
1. Acknowledges the payment did not go through
2. Explains the likely cause in plain language (no technical jargon)
3. Gives 1-2 specific actions the customer can take
4. Is 2-3 sentences maximum
Do not: mention the bank's internal error message, reveal system details, or use phrases like "error code".
The customer sees this message directly.`;
var response = await anthropic.messages.create({
model: 'claude-haiku-4-5-20251001', // fast and cheap — this runs per failed payment
max_tokens: 150,
messages: [{ role: 'user', content: prompt }],
});
var explanation = response.content[0].text.trim();
// Cache for 24 hours — same failure message = same explanation
explanationCache.set(cacheKey, explanation);
setTimeout(() => explanationCache.delete(cacheKey), 24 * 60 * 60 * 1000);
return explanation;
}
// Example outputs for common Paystack failures:
// "Do Not Honour" → "Your bank declined this payment. This can happen if your card has a daily transaction limit or if your bank flagged the transaction as unusual. Try using a different card, or contact your bank to authorize the payment."
// "Insufficient Funds" → "Your account did not have enough funds to complete this payment. Please top up your account or use a different card and try again."
// "Invalid Card Number" → "There was an issue with the card number entered. Please double-check your card details and try again, or use a different card."
Common Paystack gateway_response Values
| gateway_response | Likely Cause | Customer Action |
|---|---|---|
| Insufficient Funds | Account balance too low | Top up account or use different card |
| Do Not Honour | Bank declined without reason (fraud suspicion, limit, restriction) | Contact bank or use different card |
| Transaction Not Permitted to Cardholder | Card restricted for this transaction type | Contact bank to lift restriction or use different card |
| Invalid Card Number | Card number entered incorrectly | Check card number and re-enter |
| Expired Card | Card past expiry date | Use a different, non-expired card |
| Declined | Generic bank decline | Contact bank or use different payment method |
| Timeout | No response from bank within time limit | Try again — likely a temporary network issue |
Learn More
See building a payments support chatbot to embed this explanation pattern in a full support workflow.
Key Takeaways
- ✓gateway_response from Paystack is technical language for developers — translate it before showing to customers.
- ✓Always include a customer action in the explanation: try different card, check balance, contact bank.
- ✓Never expose raw Paystack gateway_response codes or internal error details to customers.
- ✓Vary the explanation by channel — mobile money failures differ from card failures in cause and remedy.
- ✓Cache common failure translations — the same gateway_response always produces the same explanation.
Frequently Asked Questions
- Why not just build a lookup table for failure messages instead of using an LLM?
- A lookup table is the right default for the most common failure codes — it is faster, cheaper, and deterministic. Use Claude Haiku or a small model only for failure codes not in your lookup table, or when you need the message to vary by channel (a mobile money failure explanation differs from a card failure explanation). Start with a lookup table for the 10-15 most common codes and use an LLM as fallback for the rest.
- Can I show the gateway_response directly to technical customers (developers testing integrations)?
- In a developer-facing context (test dashboard, API response for developers), yes — include the raw gateway_response alongside the friendly message. In a customer-facing checkout or payment page, never. The rule is: who is reading the message? End customers see the friendly version. Developers see both.
- Should I use Claude Opus or a smaller model for failure explanations?
- Use Claude Haiku (claude-haiku-4-5-20251001). Failure explanations are short, formulaic, and do not require complex reasoning. Haiku is fast and cheap — important because this runs on every failed transaction. Opus is for complex reasoning tasks. Save it for contexts where the reasoning complexity justifies the cost: dispute drafting, anomaly analysis, or multi-step agent workflows.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon