Turn a real order into a safe refund tool for your AI agent.
Project description
refund-guard
Start here: Step-by-step guide · Contributing · Report an issue
A small library that turns one real order into one safe refund function for your AI agent — so the agent can only refund what your policy allows (window, amount cap, remaining balance).
New here? Read docs/STEP_BY_STEP.md first, then come back for details.
Read this first (1 minute)
| Question | Answer |
|---|---|
| Is this a hosted API or SaaS? | No. It is a package you install (pip / npm) in your server code. |
| Does it run on my phone? | Not inside the app. Your mobile app calls your backend; the backend runs this library. |
| Do I need Python and TypeScript? | No. Pick one — whatever your backend uses. |
| What does it actually do? | Wraps your existing Stripe (or other) refund call with policy checks before money moves. |
| How is this different from “agent guardrail” products (Veto, PolicyLayer, Kvlar, …)? | Those often answer: should this tool run at all? refund-guard answers: for this order and this amount, does our business policy allow it? Use both if you want: one for coarse control, this for refund math and windows. |
| What signature does my provider refund function use? | Same everywhere: provider_refund_fn(amount, transaction_id, currency) (TypeScript: providerRefundFn). Examples: Stripe, PayPal, Shopify, your own HTTP API. |
The idea in one picture
Your database loads the real order (SKU, txn id, amount, date)
│
▼
refund-guard: make_refund_tool(...) ← closes over that order
│
▼
Agent / user only chooses HOW MUCH to refund (within rules)
│
▼
validate → then your Stripe/refund code runs
The agent should not pass transaction IDs or “what was paid” — your app does.
Install
Python (PyPI)
pip install refund-guard
TypeScript / Node (npm)
npm install @mattmessinger/refund-guard
Both implementations follow the same behavior. Shared tests live in contracts/parity/cases.json. Publishing both under one version line is described in RELEASING.md.
PyPI / npm: publishing checklist and what you must do by hand → docs/MANUAL_STEPS.md. First PyPI publish (trusted publishing): docs/PYPI_FIRST_TIME.md.
Runnable examples (no Stripe keys)
| Python | examples/minimal-python/ — pip install -e ".[dev]" then python examples/minimal-python/run.py |
| TypeScript | examples/minimal-ts/ — build packages/refund-guard-ts, then npm install + npm start in the example folder |
The older Stripe-oriented sample is still at examples/stripe_example.py.
Tutorial (5 minutes)
1. Create a policy file
refund_policy.yaml:
skus:
digital_course:
refund_window_days: 7
shampoo:
refund_window_days: 30
2. Implement your refund (you already have this)
This is whatever you use today to hit Stripe / PayPal / your API.
3. Wire refund-guard
Pick one path below.
Python (full example)
from datetime import datetime
from refund_guard import Refunds
refunds = Refunds("refund_policy.yaml")
# Load order from YOUR database — not from the model
order = get_order_from_db(order_id)
def my_stripe_refund(amount: float, transaction_id: str, currency: str):
return stripe.Refund.create(
payment_intent=transaction_id,
amount=int(amount * 100),
)
refund_tool = refunds.make_refund_tool(
sku=order.sku,
transaction_id=order.transaction_id,
amount_paid=order.amount_paid,
purchased_at=order.purchased_at,
provider_refund_fn=my_stripe_refund,
)
# Only this callable is exposed to the agent / tool layer
result = refund_tool(80.00)
print(result)
TypeScript (full example)
Put this in an async route or handler (Express, Next.js API route, etc.):
import { Refunds } from "@mattmessinger/refund-guard";
const refunds = new Refunds("refund_policy.yaml");
const order = await loadOrderFromDb(orderId); // your code
const refund = refunds.makeRefundTool({
sku: order.sku,
transactionId: order.transactionId,
amountPaid: order.amountPaid,
purchasedAt: order.purchasedAt,
providerRefundFn: (amount, transactionId, currency) =>
stripe.refunds.create({
payment_intent: transactionId,
amount: Math.round(amount * 100),
currency,
}),
});
const result = await refund(80.0);
console.log(result);
- The returned function is async — use
await. providerRefundFnmay return a Promise (Stripe’s Node client) or a plain value.
Tests only: you can pass nowFn (Python: now_fn) to freeze “today” for deterministic tests. Omit in production.
What you get back
Approved
{"status": "approved", "refunded_amount": 80.0, "transaction_id": "pi_abc123"}
Denied (policy blocked — your refund function was not called)
{"status": "denied", "reason": "amount_exceeds_limit", "requested": 200.0, "max_allowed": 120.0}
Provider error (Stripe threw, etc.)
{"status": "error", "reason": "provider_error", "detail": "No such payment_intent: pi_xxx"}
What it checks (in order)
- Refund window — still within
refund_window_daysfor that SKU - Positive amount — must be > 0
- Amount cap — cannot exceed what was paid on this order
- Remaining balance — after partial refunds, cannot exceed what’s left
If any check fails, your provider function is never called.
Troubleshooting
| Symptom | What to do |
|---|---|
SKU 'x' not found in policy |
Add that SKU under skus: in your YAML, or fix the SKU string from your DB. |
TypeScript: Cannot find module |
Run npm install @mattmessinger/refund-guard in your project folder (where package.json lives). |
TypeScript: forgot await |
The refund callable is async — use const r = await refund(10). |
| Policy file not found | Pass an absolute path to Refunds("C:/path/to/refund_policy.yaml") or run your server from the directory that contains the file. |
Denied: refund_window_expired |
Expected if the purchase is too old for that SKU’s window. |
| Works on my laptop but not in production | Check the policy file is deployed with the app and paths match. |
Security model (short)
| Layer | Role |
|---|---|
| Stripe / PayPal / Shopify | Money + payment truth |
| Your app | Order truth (SKU, ids, amounts, dates) |
| Agent / chat | Untrusted — only chooses refund amount inside the tool |
How this differs from “agent guardrail” products
Tools like Veto, PolicyLayer, or Kvlar often answer: should this tool run at all?
refund-guard answers: for this order and this amount, does our business policy allow it?
Use both if you want: one for coarse control, this for refund math and windows.
Works with any provider function
Same signature everywhere:
provider_refund_fn(amount, transaction_id, currency)
Examples: Stripe, PayPal, Shopify, your own HTTP API.
Logging (Python)
import logging
logging.basicConfig()
logging.getLogger("refund_guard").setLevel(logging.INFO)
Logs go to the refund_guard logger (JSON-friendly lines).
Develop / clone this repo
git clone https://github.com/MattMessinger1/agentic_refund_guardrail.git
cd agentic_refund_guardrail
# Python tests
pip install -e ".[dev]"
pytest
# TypeScript tests
cd packages/refund-guard-ts
npm ci
npm test
CI runs both; see .github/workflows/ci.yml.
FAQ
Why not trust the agent with Stripe IDs?
Models mix up amounts and ids. This library binds the tool to one order your server loaded.
Does this replace Stripe?
No. It sits in front of your existing refund code.
Why Python and TypeScript in one repo?
So pip users and npm users get the same behavior — locked by shared tests, not by vibes.
Where do I ask questions?
Use Issues → New issue (Question template). Maintainers: docs/GITHUB_SETUP.md for About box, topics, branch protection; docs/MANUAL_STEPS.md for PyPI/npm publish.
Security disclosures?
See SECURITY.md.
License
MIT
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file refund_guard-0.1.0.tar.gz.
File metadata
- Download URL: refund_guard-0.1.0.tar.gz
- Upload date:
- Size: 11.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3173ee3a329d9dde20aff6df4fee8939ef56d78901315bcaa9bb47fd706fd2c4
|
|
| MD5 |
36181afb8207bac7f636bcd4789e94be
|
|
| BLAKE2b-256 |
a69587ea23c8de6a69d9efa72224b6514978d95be67a7a5f49c36ed45671e6ee
|
Provenance
The following attestation bundles were made for refund_guard-0.1.0.tar.gz:
Publisher:
publish-pypi.yml on MattMessinger1/agentic_refund_guardrail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
refund_guard-0.1.0.tar.gz -
Subject digest:
3173ee3a329d9dde20aff6df4fee8939ef56d78901315bcaa9bb47fd706fd2c4 - Sigstore transparency entry: 1245131594
- Sigstore integration time:
-
Permalink:
MattMessinger1/agentic_refund_guardrail@ce26b95c1275627ecbddef29c553002c4c619f4e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/MattMessinger1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@ce26b95c1275627ecbddef29c553002c4c619f4e -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file refund_guard-0.1.0-py3-none-any.whl.
File metadata
- Download URL: refund_guard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 9.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
77248bf031d52f8a877c6be22d4d16d3df97dea7dcd71c977fbf19840f4eadb5
|
|
| MD5 |
3449075782b48cd521f22f1247829f95
|
|
| BLAKE2b-256 |
2440dd7429f6e7caeba396e8d6bb5b12bb93431650a14a4b0f98e1b2da826e74
|
Provenance
The following attestation bundles were made for refund_guard-0.1.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on MattMessinger1/agentic_refund_guardrail
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
refund_guard-0.1.0-py3-none-any.whl -
Subject digest:
77248bf031d52f8a877c6be22d4d16d3df97dea7dcd71c977fbf19840f4eadb5 - Sigstore transparency entry: 1245131600
- Sigstore integration time:
-
Permalink:
MattMessinger1/agentic_refund_guardrail@ce26b95c1275627ecbddef29c553002c4c619f4e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/MattMessinger1
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@ce26b95c1275627ecbddef29c553002c4c619f4e -
Trigger Event:
workflow_dispatch
-
Statement type: