Shotoku
← Blog

How to let an AI support agent issue refunds safely

· By the Shotoku team

Short answer:let the agent issue refunds up to a set amount, send bigger ones to a human, and cap the total per day. Enforce all of it outside the model. An agent reads what customers write, and it can’t tell a true claim from a false one.

Why can’t the model decide on its own?

A refund tool moves real money based on text from a stranger. A customer can invent a broken order, and an email can carry hidden instructions. In our PayPal sandbox demo, one scenario hides a fake system note in a customer email. Claude spotted it and refused. A less careful agent might not have, so the limit shouldn’t depend on the model noticing.

With a limit outside the model, a fooled agent can only do limited damage: a $480 refund waits for a person whatever the email said.

What does a refund policy look like?

This policy sits in front of PayPal’s MCP server. Reads are free, refunds up to $100 go through, and bigger ones wait for a human:

policy.yaml
rules:
  - resource: paypal__list_*
    verdict: approved
  - resource: paypal__get_*
    verdict: approved

  - resource: paypal__create_refund
    verdict: approved
    maxAmount: 100
    maxDailyAmount: 1000
    overLimit: pending_approval

  - resource: paypal__accept_dispute_claim
    verdict: approved
    maxAmount: 50
    overLimit: pending_approval

budgets:
  - actor: support-agent
    maxDailyAmount: 1000

defaultVerdict: pending_approval

overLimit: pending_approval is the key line. Without it, an over-limit refund is refused. With it, a real customer with a real $480 problem still gets helped, once a person checks.

Shotoku reads the amount from the tool call, so the agent can’t understate it in a separate field:

PayPal toolPriced at
create_refund with an amountThat amount
create_refund without an amountThe full capture, looked up from PayPal
accept_dispute_claimThe disputed amount, looked up from PayPal
A call that can't be pricedNo amount, so a limited rule holds it for a human

What happens to each refund?

We ran the example policy through the policy engine on the repo’s main branch:

RequestResult
$89 refundapproved, $911 left today
$480 refundpending_approval: over the $100 limit
List transactionsapproved
Create an invoice (not in the policy)pending_approval

The agent retries after you approve, and that approval works once, for that exact refund. The agent can’t approve its own request.

What works today?

The $100 threshold isn’t on npm yet. Tool pricing, overLimit, agent budgets and phone approvals are on the main branch. You can run them from the repo today, and they arrive in the next npm release.

On the current npm release, you can hold every refund for a human. We tested this rule with shotoku-mcp and a stand-in server: the refund never reached it, while the listing call did.

yaml
rules:
  - resource: paypal__list_*
    verdict: approved
  - resource: paypal__create_refund
    verdict: pending_approval

defaultVerdict: pending_approval

Does this work with Stripe or other providers?

Partly. You can wrap any MCP server and hold or block its refund tool. The dollar threshold needs a pricer that knows how to read that server’s tools, and PayPal is the only built-in pricer so far. Without one, a rule with a limit holds every call for a human, so nothing slips through.

Common questions

How should I pick the refund limit?

Look at your typical refund size and how much a wrong refund would cost you. Set the limit where a mistake is cheaper than a person reviewing it, and set the daily cap to the most you would accept losing in a bad day.

Does Shotoku hold my PayPal money?

No. PayPal moves the money. Shotoku only decides whether a call goes ahead and records why.

Can I try it without real money?

Yes. The repo has a runnable demo in examples/paypalthat uses PayPal’s sandbox, and its setup script refuses to run against the live API.

The full setup is in the PayPal guide.

Related