Skip to main content

Turn a real order into a safe refund tool for your AI agent.

Project description

refund-guard

CI License: MIT Python 3.10+ Node 18+

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.
  • providerRefundFn may 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)

  1. Refund window — still within refund_window_days for that SKU
  2. Positive amount — must be > 0
  3. Amount cap — cannot exceed what was paid on this order
  4. 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

refund_guard-0.1.0.tar.gz (11.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

refund_guard-0.1.0-py3-none-any.whl (9.4 kB view details)

Uploaded Python 3

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

Hashes for refund_guard-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3173ee3a329d9dde20aff6df4fee8939ef56d78901315bcaa9bb47fd706fd2c4
MD5 36181afb8207bac7f636bcd4789e94be
BLAKE2b-256 a69587ea23c8de6a69d9efa72224b6514978d95be67a7a5f49c36ed45671e6ee

See more details on using hashes here.

Provenance

The following attestation bundles were made for refund_guard-0.1.0.tar.gz:

Publisher: publish-pypi.yml on MattMessinger1/agentic_refund_guardrail

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for refund_guard-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 77248bf031d52f8a877c6be22d4d16d3df97dea7dcd71c977fbf19840f4eadb5
MD5 3449075782b48cd521f22f1247829f95
BLAKE2b-256 2440dd7429f6e7caeba396e8d6bb5b12bb93431650a14a4b0f98e1b2da826e74

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page