Inspiration

You know, there is a small moment that most of us know well. Something we bought stops working, we explain the problem in a few rushed words, and someone, or something, tells us it has been taken care of. We relax and then, days later, we find out that nothing was taken care of at all.

Voice assistants can already say "I have started your return". What they cannot always do is know whether that sentence is true. A customer tells the story in pieces ("my headphones keep disconnecting"). The retailer holds the facts, scattered across several orders and more than one version of its return policy. And sometimes the response to a request simply disappears, after the retailer has already acted on it. An assistant that guesses, or that hears "yes, go ahead" and treats it as consent, can turn down a valid return, send back the wrong item, or tell someone that something happened when it never did.

AFTERCARE grew out of one question we could not stop asking: when is it actually safe for an assistant to take the next step, and what has to be proven before it is allowed to say "done"?

What it does

AFTERCARE is a self hosted MCP server (spec 2025-11-25, Streamable HTTP) that an Alexa+ style assistant can call when a customer needs help with something they bought. It offers ten typed tools: start, continue, serial number, approve, decline, submit, check outcome, get case, list cases and explain decision.

Our flagship scenario follows a customer who owns two identical pairs of headphones, bought under two different return policies. This is what AFTERCARE does, step by step:

  1. It gathers the orders, along with the policy version that applied on each purchase date.
  2. It does not guess which pair the customer means. It asks, and a serial number settles it.
  3. It drafts a return slip with the exact terms: order, line, reason, refund, shipping and deadline.
  4. It waits for an explicit approval of those exact terms. A cheerful "yes" in conversation is noted, but it does not count.
  5. It writes the operation to the database before it contacts the retailer.
  6. If the retailer's response is lost, it marks the outcome as unknown and checks the retailer's records, rather than sending the request again blindly.
  7. It reads the return back from the retailer and compares it with what the customer approved. Only then does the case become Confirmed.

Every case persists. The customer can reload the page, or the server can restart, and the case picks up exactly where it left off, with its full history intact.

How we built it

  • A deterministic Resolution Compiler. At the heart of AFTERCARE is a pure function. It takes the case, the evidence, the proposals and the operations, and returns the next justified step. The same snapshot always gives the same answer, and every decision is stored, so the Resolution Lab can show exactly what was decided and why.
  • Approvals bound to a fingerprint. Each approval records the SHA-256 digest of the exact proposal. If the evidence changes, that proposal is withdrawn, and the old approval no longer authorises anything.
  • Write ahead operations, reconciliation and postcondition verification. In AFTERCARE, "accepted" means submitted. It does not mean done.
  • A stateless MCP endpoint. One server and one transport per request, JSON responses, and GET and DELETE return 405.
  • A simulated retailer. Kestrel Goods is entirely fictional. AFTERCARE can only reach it through a gateway port, so a lost response creates genuine uncertainty that has to be resolved by asking.
  • Two surfaces. An assistant page that acts as an MCP client, and a Resolution Lab that opens up the decision trace, the evidence, the action lifecycle, the event timeline and the retailer's own records.
  • An optional language model interpreter, switched off by default. Whatever a model says is treated only as a customer claim. It cannot approve, decide eligibility, choose a policy or submit anything. The rules goal always wins, a reason supplied by the model is accepted only when it quotes the customer's own message, and serial and order numbers always come from regular expressions.

Stack: TypeScript on Node 24, Express 5, SQLite through node:sqlite, zod, the official MCP TypeScript SDK, React 19 with Vite, and Vitest and Playwright with axe.

Measured results

We did not want to simply claim that AFTERCARE is careful, so we built a way to measure it. The repository includes an evaluation harness with 22 scenarios, run against AFTERCARE and two reference agents that share the same tools and the same language interpreter. The baseline has none of AFTERCARE's controls. The careful baseline adds three fixes that need no case model.

Strategy Correct outcomes, 22 scenarios Original 14 scenarios
AFTERCARE 22 14
Careful baseline 16 9
Baseline 11 7

Across all 22 scenarios, AFTERCARE recorded no wrongful denials, no authorisation violations, no wrong terms submitted, no duplicate attempts and no confirmations before verification. The baseline, by comparison, confirmed 10 returns before it had verified them. We want to be clear about what these numbers are: results on synthetic scenarios that we wrote ourselves, with baselines that serve as reference points rather than measurements of any real product. Every scenario lives in the repository, and anyone can rerun them with npm run eval.

Challenges and what we learned

  • Lost responses are the real problem. A timeout after sending tells you nothing about whether the retailer acted. Designing the write ahead record, the stable idempotency key and the reconcile step took most of our effort, and taught us the most.
  • Elicitation does not work on a stateless endpoint. We tried to ask the customer a question in the middle of a call through MCP elicitation. Server to client requests cannot complete without a session, and the error message points to the wrong cause. We chose to keep the endpoint stateless, documented the limitation, and moved the question into the case state instead. We have shared this in our friction log and product feedback.
  • A small model helped in some places and hurt in others. On 30 hand labelled messages, a small local model got the customer's reason right more often, but it sometimes overrode a correct rules goal and sometimes supplied a reason the customer never gave. We added a goal precedence rule and a quote check. The quote check did not fully solve the reason problem, because a model can quote the request itself as the reason. We report this openly in the repository rather than adjusting the labels to make the numbers look better.
  • Idempotency is not exactly once. Our sandbox honours idempotency keys because we built it that way. A real retailer might not, and our documentation says so.

What's next

We would love to connect AFTERCARE to a real retailer interface, support more policy families and item categories, and add a session capable transport for the moments when an assistant genuinely needs to ask the customer something mid call.

AFTERCARE is a simulation. The retailer, the orders and the policies are fictional, and it is not an Amazon product.

You can find the source, documentation, threat model and known limitations in the repository.

Built With

Share this project:

Updates

Submission history