All systems operational

Resolve

Dispute inbox for Paystack teams. Paste a “you took my money” message or receipt screenshot — Resolve verifies it live against Paystack and your order ledger, then drafts the reply for one-click approval in under 60 seconds.

  • Next.js
  • TypeScript
  • Tailwind CSS
  • Supabase
  • Groq
Resolve landing page on a laptop — A watchdog that checks Paystack, a clerk that writes the reply

Why I built this

Every business on Paystack lives the same ticket: a customer pays, gets debited, sees “pending” or no receipt, and sends a panicked message — often to WhatsApp, Instagram, and email at the same time. A support agent then opens the Paystack dashboard, searches the reference, reads the status, and switches back to reply. Ten to fifteen minutes per ticket, and during a sales spike a hundred of them pile up at once.

Under that pressure agents guess — “network issue, please wait” — or refund incorrectly. Both cost money and trust. And the existing AI tools don't help here: Intercom Fin and Zendesk AI generate answers from conversational context without ever calling GET /transaction/verify/:reference, so they can confidently state a payment status they never checked. That's fine for a password-reset bot. It's not fine when the answer decides whether money moves.

Resolve exists for that gap: no reply is ever drafted without a live Paystack lookup behind it. A detective that checks Paystack, and a cashier that writes the reply based only on what the detective found.

My approach

Resolve is a Next.js app built around a four-stage pipeline: extract, verify, draft, approve. Paste a message or drop a receipt screenshot — text only, image only, or both. An LLM reads out the reference, amount, phone, and email as strict JSON, with a regex fallback so the pipeline degrades instead of breaking.

Verify is the core. It calls Paystack live, cross-checks the business's own order ledger in Supabase to separate “paid but not fulfilled” from “paid and already fulfilled,” and scans the refund list to block double refunds. That resolves to one of seven verdicts: success unfulfilled or fulfilled, failed funds, abandoned, no reference, already refunded, or verify error.

Draft is deliberately crippled: it receives the verdict and its facts only — never the raw message, never the image — and the request is rejected outright if those keys show up. A human always approves; nothing auto-sends. Every approval writes to an append-only audit log, and customer screenshots are processed in memory and never persisted anywhere.

Key features

  • Messy input welcome. Paste text, drag in a bank-alert screenshot, or both — cross-referenced when they disagree
  • Seven real verdicts. Paid, failed, abandoned, refunded, or needs-human — every path handled, not just the success case
  • Verdict-specific actions. Resend receipt, send retry link, fresh payment link, or a disabled refund button when one already exists
  • An audit trail that holds up. Timestamp, actor, action, verdict, extraction method, amount — no edit, no delete
  • Minutes-saved counter. Each approval ticks up the running total for demos and reporting
  • Privacy by construction. Restricted Paystack keys in httpOnly cookies, images never stored, no card data anywhere

How the system works

A panicked message goes in one side; an approved, verified reply comes out the other in under a minute.

  1. Connect a restricted Paystack key in setup — validated with a real API call, stored httpOnly, never client-exposed
  2. Paste the customer message or drop a receipt screenshot into the inbox composer (4MB cap, in-memory only)
  3. Extract pulls reference, amount, phone, and email — LLM first, regex fallback if it fails or times out
  4. Verify hits Paystack live, cross-checks the Supabase order ledger, and scans for existing refunds to reach a verdict
  5. Draft writes a short plain-language reply from the verdict facts alone, or a forced escalation template when there's nothing to confirm
  6. Approve (or edit, or take the verdict-specific action) — the audit log records exactly what was checked and sent

Challenges & lessons learned

The draft route was leaking. It originally accepted the extraction alongside the verify result, which meant drafts could reference things Paystack never confirmed — the exact hallucination the product exists to prevent. I stripped the input to VerifyResultSchema only and made the route reject requests carrying extraction, message, or image keys. A rejected field is a loud signal if the leak is ever reintroduced; a silently dropped one is not.

Auth hardening couldn't wait for post-pilot. A redirect-only proxy is a UI control — without row-level security, the audit log stays readable across businesses at the database layer. So the migration ships the user_id column, RLS enablement, policies, and grant revokes in one file, with no update or delete policy at all: an audit log you can edit isn't an audit log.

Smaller calls with the same philosophy: verdict names follow the tested code, not the planning doc — renaming working enums to match a document is pure risk. Image privacy holds by construction, because the approve payload is allowlisted and the image simply isn't in it. And if Paystack is unreachable, verify fails into “needs human,” never into a false success or failure.

What's next

  • Pilot with a real Lagos business on live traffic — the proof point everything else serves
  • Pin the LLM provider on evidence: same three screenshots (one deliberately messy) through both candidates, bar set before the test
  • Time one real manual ticket end-to-end so the minutes-saved counter traces to an observation, not an estimate
  • Later, explicitly deferred: WhatsApp auto-send, more gateways, analytics beyond the counter

You made it this far

Let's build something exceptional together.

Tell me about your project and I'll help you shape it into a polished, high-performing experience.