Refund audit
burnbound-mcp audit-refunds reads your Stripe refunds of the last days and replays them against a Burnbound policy, by default the support refunds pack. It tells you how many refunds Burnbound would have denied or held for approval, by which rule and for how much. It runs on your machine, needs no Burnbound account and sends nothing to Burnbound.
What leaves your machine?
- To Stripe: read-only requests to
api.stripe.com(GET /v1/refundsandGET /v1/customers/:id), with your key. Nothing else ever gets the key. - To Frankfurter: one request without your key for the European Central Bank's reference rates of the day, only when some refund is not in USD and you did not pass
--usd-rates. - To Burnbound: nothing. The report is printed or written to the file you name.
The key is never written to disk, never printed and never shown in an error: any message that could carry it masks it.
How do I run it?
- In Stripe, create a restricted key (Developers → API keys → Create restricted key) with Charges, Refunds and Customers set to Read, and everything else to None. A test mode key also works.
- Put it in the environment without leaving it in your shell's history, and run the audit (Node.js 20 or later):
read -rs STRIPE_API_KEY && export STRIPE_API_KEY
npx -y @burnbound/mcp audit-refunds
The key comes from STRIPE_API_KEY or from a file with --key-file <path>. A key passed as an argument is refused, because it would stay in the history and in the process list. A live mode secret key (sk_…) is refused unless you add --allow-secret-key: a restricted read-only key is safer.
Which options are there?
| Option | What it does |
|---|---|
--days <1-365> |
How far back to audit. Default 90. |
--pack <id> |
The policy pack to replay. Default support-refunds. |
--rules <file.json> |
Your own rules instead of a pack: a JSON list of Burnbound custom rules, or { "rules": [...] }. |
--usd-rates <rates> |
USD per unit of each currency, such as EUR=1.10,GBP=1.28. Default: the ECB's reference rates of today. |
--format <format> |
text (default), json or markdown. |
--out <file> |
Write the report to this file, readable only by you, instead of printing it. |
--redact |
Replace customer, charge and refund ids with short stable hashes (customer-3f9a1c02), to share the report. |
--max-refunds <n> |
Read at most this many refunds. Default 20000. |
--key-file <path> |
Read the Stripe key from this file. |
--allow-secret-key |
Accept a live mode secret key. |
Exit codes: 0 the report was written, 1 the audit could not run (a rejected key, Stripe or the rates unreachable), 2 wrong usage. Stripe's 429 and 5xx answers are retried up to five times, honouring Retry-After.
What does it check?
The support-refunds pack has four rules, the same ones you can apply to an agent in Burnbound:
tpl-refund-amount-approval: approval for a refund over 100 USD.tpl-refund-customer-monthly-limit: one refund per customer per calendar month (UTC); the next one is denied. A refund that was denied does not use up the month.tpl-refund-old-charge-approval: approval for a refund of a charge older than 30 days.tpl-refund-disputed-deny: no refund of a disputed charge.
Each refund is judged with the facts as they were when it was made: the charge's amount already refunded, the customer's age and the customer's refunds of the 90 days before. They are built the same way the action gateway builds them for a live refund. A deny beats a hold, and the report names every rule that matched.
With --rules, every rule is replayed as enforced, even one marked observe. Rules are written as in Concepts.
What does it not see?
- Only the rules. The agent's own caps (per refund, daily, allowed hosts) need its policy and spend, which a local run does not have: they are not evaluated.
- Today's rates. Amounts in other currencies are converted to USD with the rates of today, not of each refund's day. A currency without a rate is not evaluated, and the report says which.
- Refunds that moved money. Failed and canceled refunds are left out; pending ones count.
- Long histories. If
--max-refundscuts the read, the oldest refunds of the window are missing and some customer histories are short: the report warns about both. - Customers. A key without Customers: Read still works, but every customer's age is unknown and rules on it take the restrictive branch.
What does the JSON report look like?
--format json prints one object with auditFormat: 1. New fields may be added; an incompatible change raises auditFormat.
{
"auditFormat": 1,
"window": {
"from": "2026-08-05T00:00:00.000Z",
"to": "2026-11-03T00:00:00.000Z",
"days": 90
},
"policy": {
"source": "pack",
"packId": "support-refunds",
"rules": [{ "id": "tpl-refund-disputed-deny", "action": "deny" }]
},
"rates": {
"source": "ecb",
"date": "2026-11-02",
"usdPerUnit": { "EUR": "1.0834" }
},
"totals": { "refunds": 1284, "usd": "48210.4", "unpriced": 0 },
"wouldDeny": {
"refunds": 37,
"usd": "2917.6",
"byRule": [
{ "ruleId": "tpl-refund-disputed-deny", "refunds": 6, "usd": "1072.5" }
]
},
"wouldHold": { "refunds": 58, "usd": "9934.2", "byRule": [] },
"topCustomers": [
{ "customer": "customer-8d21e0b7", "refunds": 9, "usd": "1240.3" }
],
"affected": [
{
"at": "2026-10-14T09:12:00.000Z",
"refundId": "refund-3f9a1c02",
"chargeId": "charge-0b6e7d19",
"customer": "customer-8d21e0b7",
"amount": "149.00 EUR",
"amountUsd": "161.43",
"decision": "step_up",
"rules": ["tpl-refund-amount-approval"]
}
],
"warnings": [
{
"code": "base_layer_not_evaluated",
"message": "Only the custom rules were replayed. …"
}
],
"nextStep": {
"message": "Turn these rules on in Burnbound: …",
"url": "https://burnbound.dev/register"
}
}
USD amounts are exact decimal strings. affected lists the refunds that would have been denied (deny) or held (step_up), oldest first. The warning codes are base_layer_not_evaluated (always), history_truncated, currency_without_rate, max_refunds_reached, customers_unreadable, refunds_without_charge and refunds_not_counted.
How do I turn the rules on?
To stop those refunds for real, sign up, give your support agent its Burnbound key and apply the support refunds pack in the agent's rules. Refunds then go through the action gateway you host: see the stripe_refund tool in the MCP tools reference.