The integration that worked perfectly until it didn't
I run a small dev shop in Nairobi. We build e-commerce sites for boutiques, hardware stores, the occasional chama that wants to sell member products online. Most of our clients use M-Pesa—customers pay via STK push, we get a webhook callback, we mark the order paid. Standard Daraja API integration. I've written this flow maybe twenty times.
Two weeks ago a client called at 1:47 a.m. Her site sold beaded jewellery to diaspora buyers and local walk-ins. Orders were going through, customers were getting the M-Pesa prompt, payments were leaving their accounts, but the site never marked anything paid. She had six angry WhatsApp messages and a Jumia competitor who'd just promised same-day delivery.
I logged into the server. The webhook endpoint was returning 200. The M-Pesa callback was hitting our URL—I could see it in the access logs. But our database showed zero completed orders. Something was timing out between the callback arriving and our code processing it, and I couldn't see where.
I pasted the webhook handler into three models: Claude Sonnet 4.5, Gemini Pro, and DeepSeek V3. Same code, same prompt, same 2 a.m. desperation. One of them found it in fifteen seconds. One gave me logging advice that helped but didn't solve it. One told me the code looked fine.
The code that was supposed to work
Here's the webhook handler, simplified but real:
@app.route('/mpesa/callback', methods=['POST'])
def mpesa_callback():
data = request.get_json()
transaction_id = data['Body']['stkCallback']['CheckoutRequestID']
result_code = data['Body']['stkCallback']['ResultCode']
if result_code == 0:
# Payment successful
order = Order.query.filter_by(checkout_id=transaction_id).first()
order.status = 'paid'
db.session.commit()
# Send confirmation SMS
send_sms(order.customer_phone, f"Payment received. Order {order.id} confirmed.")
# Update inventory
for item in order.items:
product = Product.query.get(item.product_id)
product.stock -= item.quantity
db.session.commit()
return jsonify({'ResultCode': 0, 'ResultDesc': 'Success'}), 200
It's a Flask app. M-Pesa hits the endpoint, we parse the callback, mark the order paid, send an SMS, update stock. I'd tested it on sandbox. It worked on three other client sites. I stared at it for forty minutes and saw nothing wrong.
Claude Sonnet 4.5: found the timeout in one response
I opened Kryotta, picked Claude Sonnet 4.5, pasted the code and wrote: "M-Pesa callback is hitting this endpoint but orders aren't getting marked paid. Access logs show 200 response. What's timing out?"
Claude's first paragraph: "M-Pesa expects a response within 30 seconds or it retries the callback. Your code is doing three slow operations—database commit, SMS send, inventory update—before returning the 200. If any of those takes more than a few seconds, M-Pesa times out and retries, which means you process the same payment twice or the callback fails entirely. Move everything after the initial commit into a background task."
That was it. The SMS provider we used (a local Kenyan gateway) sometimes took eight to twelve seconds to return. If the inventory update hit a locked row, add another three seconds. M-Pesa would give up, retry the callback, and our code would try to update an order that was mid-commit from the first attempt. Race condition, database lock, timeout loop.
Claude suggested this fix:
@app.route('/mpesa/callback', methods=['POST'])
def mpesa_callback():
data = request.get_json()
transaction_id = data['Body']['stkCallback']['CheckoutRequestID']
result_code = data['Body']['stkCallback']['ResultCode']
if result_code == 0:
order = Order.query.filter_by(checkout_id=transaction_id).first()
order.status = 'paid'
db.session.commit()
# Queue the slow stuff
background_tasks.add_task(send_sms, order.customer_phone, f"Payment received. Order {order.id} confirmed.")
background_tasks.add_task(update_inventory, order.id)
return jsonify({'ResultCode': 0, 'ResultDesc': 'Success'}), 200
Return the 200 immediately, handle SMS and inventory in a Celery task. I deployed it at 2:31 a.m. The next payment went through in four seconds. No retries, no timeouts, order marked paid before the customer's screen even refreshed.
Gemini Pro: useful logging, missed the root cause
I ran the same code and prompt through Gemini Pro. It gave me a longer answer—about 280 words—that focused on error handling and observability. It suggested wrapping the SMS call in a try-except block, adding logging before and after each database commit, and checking whether order was None before trying to update it.
All good advice. I should've had that logging from the start. But Gemini didn't mention the timeout. It assumed the 200 response meant M-Pesa was happy, so the problem must be an exception we weren't catching or a missing order record. When I followed up with "Could this be a timeout issue?", it agreed and suggested the background task pattern, but it didn't lead with that.
If I'd only used Gemini, I would've added better logs, waited for the next payment to fail, read the logs, and eventually figured out the timeout myself. Faster than staring at the code, but not as fast as Claude handing me the answer.
DeepSeek V3: confident and wrong
DeepSeek V3 told me the code looked fine. It said the logic was correct, the error handling was missing but not critical, and I should check whether the M-Pesa callback payload was malformed or the database connection was dropping. It suggested I add a print statement to confirm the callback was arriving.
The callback was arriving. The payload was fine. The database connection was fine. DeepSeek didn't consider timing or concurrency at all. When I asked explicitly about timeouts, it said "Flask should handle that automatically," which is not how M-Pesa webhooks work.
I don't think DeepSeek is a bad model. I've used it for other tasks—refactoring, writing tests, explaining unfamiliar libraries—and it's been helpful. But for debugging a production payment flow at 2 a.m., it didn't ask the right questions.
The pattern I noticed: reasoning depth vs speed
Claude found the issue because it modelled what happens over time. It didn't just check whether each line of code was correct; it traced the sequence—callback arrives, code runs, SMS call blocks, M-Pesa waits, timeout, retry—and spotted the gap.
Gemini gave me the building blocks. Better logging would've led me to the timeout eventually. But it started with the assumption that the code was mostly fine and I just needed more visibility.
DeepSeek checked syntax and logic but didn't simulate the interaction between my code and M-Pesa's retry behaviour. It treated the webhook as a pure function instead of part of a distributed system with timing constraints.
This matches what I've seen in other debugging sessions. Claude Sonnet 4.5 is the model I reach for when something is broken and I don't know why. Gemini Pro is good when I have a hypothesis and need to check it or when I want a second opinion. DeepSeek is fine for refactoring or writing new code, but I don't trust it to debug concurrency or timing issues.
When to use which model for M-Pesa (or any payment API)
| Task | Model | Why |
|---|---|---|
| Webhook isn't working, no idea why | Claude Sonnet 4.5 | Traces interactions over time, spots timing and concurrency issues |
| Error is logged, need to understand it | Gemini Pro | Good at explaining stack traces and suggesting observability improvements |
| Refactor working code | DeepSeek V3 or Gemini | Fast, handles structure and style well |
| Write tests for a payment flow | Claude Sonnet 4.5 | Thinks through edge cases (retries, partial failures, idempotency) |
| Check if code matches Daraja docs | Gemini Flash | Fast, good at comparing code to reference documentation |
I keep a Kryotta workspace open with Claude, Gemini and DeepSeek in separate chats. When a client calls with a broken integration, I start with Claude. If I need a second opinion or I'm choosing between two fixes, I'll paste the options into Gemini. DeepSeek I use for the boring stuff—renaming variables, splitting a long function, writing docstrings—because it's fast and I don't need deep reasoning for that.
The fix in production: three follow-up changes
After I deployed the background task fix, I made three more changes based on what Claude and Gemini suggested:
- Idempotency check: Before marking an order paid, check if it's already paid. M-Pesa can retry even with the fast response, and I don't want to send two confirmation SMSs.
- Logging at every step: Log when the callback arrives, when the order is marked paid, when the background task starts. Costs me nothing, saves me an hour next time something breaks.
- Timeout on the SMS call: The SMS gateway has no SLA. I wrapped the call in a timeout so a slow response doesn't block the background worker.
The site has processed 140 payments since then. Zero timeouts. The client hasn't called me at 2 a.m. again, which is my main KPI.
Questions people ask
Which AI model is best for debugging M-Pesa integrations?
Claude Sonnet 4.5. It's the only one that consistently thinks about timing, retries and concurrency—exactly what breaks in webhook flows. Gemini Pro is a solid second choice if you already have logs and need help interpreting them.
Can I use DeepSeek for payment API debugging?
I wouldn't. It's good at checking syntax and logic, but it doesn't model distributed system behaviour well. Use it for refactoring or writing tests after you've fixed the bug.
Do I need a separate tool for each model, or can I compare them in one place?
Kryotta lets you run Claude, Gemini and DeepSeek side by side in Compare mode, or you can open separate chats in a workspace. I keep them in separate chats because I'm usually iterating on one model's answer, not comparing all three every time.
How much does it cost to debug production code with Claude on Kryotta?
Kryotta pricing is in KSh and USD. A typical debugging session—paste code, ask three follow-up questions, get a fix—uses maybe 8,000 tokens total. That's cheap enough that I don't think about it when a client's payment flow is down.
I'm not saying you need AI to debug a webhook. I've fixed plenty of M-Pesa integrations with print statements and the Daraja API docs. But when it's 2 a.m. and you can't see the issue, having a model that thinks through timing and retries is faster than guessing. Claude found my timeout bug in fifteen seconds. I would've found it eventually, but "eventually" doesn't help when your client has six angry customers and a competitor promising same-day delivery. Try it yourself at Kryotta—open a workspace, paste your broken webhook, and see which model gets you back to sleep fastest.



