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 · Integration 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 refund call with policy checks before money moves.
How is this different from agent guardrail products (Veto, PolicyLayer, Kvlar, etc.)? Those 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.
Do I have to use a YAML file? No. Pass a plain object: Refunds({ skus: { my_sku: { refund_window_days: 14 } } }). YAML is available when you have many SKUs or want non-engineers to edit policy.
What signature does my provider function use? (amount, transaction_id, currency) — same for Stripe, PayPal, Shopify, or your own HTTP API.

The idea in one picture

Your database loads the real order (SKU, txn id, amount, date)
        |
        v
  refund-guard: make_refund_tool(...)   <-- closes over that order
        |
        v
  Agent / user only chooses HOW MUCH to refund (within rules)
        |
        v
  validate -> then your provider 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, enforced by shared tests in contracts/parity/cases.json.


Examples

Python examples/minimal-python/ -- fake provider, runs instantly
TypeScript examples/minimal-ts/ -- fake provider, build TS package first
Real-world pattern examples/real-world-ts/ -- annotated reference showing DB fetch, unit conversion, result mapping (not runnable)

Tutorial (5 minutes)

1. Define your policy

# Inline (simplest)
refunds = Refunds({"skus": {"shampoo": {"refund_window_days": 30}}})

# Or from a YAML file (when you have many SKUs)
refunds = Refunds("refund_policy.yaml")

2. Wire refund-guard

Pick one language.


Python (full example)

from datetime import datetime
from refund_guard import Refunds

refunds = Refunds({"skus": {"shampoo": {"refund_window_days": 30}}})

order = get_order_from_db(order_id)  # YOUR database

def my_refund(amount: float, transaction_id: str, currency: str):
    # Your existing Stripe / PayPal / Shopify / HTTP call
    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_refund,
)

result = refund_tool(80.00)
print(result)

TypeScript (full example)

import { Refunds } from "@mattmessinger/refund-guard";

const refunds = new Refunds({ skus: { shampoo: { refund_window_days: 30 } } });

const order = await loadOrderFromDb(orderId);

const refund = refunds.makeRefundTool({
  sku: order.sku,
  transactionId: order.transactionId,
  amountPaid: order.amountPaid,
  purchasedAt: order.purchasedAt,
  providerRefundFn: (amount, transactionId, currency) =>
    // Your existing Stripe / PayPal / Shopify / HTTP call
    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 or a plain value.
  • Tests only: pass nowFn (Python: now_fn) to freeze "today." Omit in production.

API reference (quick)

Refunds(policy)

Param Type Notes
policy YAML file path or plain object { skus: { sku_name: { refund_window_days: N } } } Loaded once; reuse the instance

refunds.makeRefundTool(opts) / refunds.make_refund_tool(**opts)

Option Type Required Default
sku string yes --
transaction_id / transactionId string yes --
amount_paid / amountPaid number yes --
purchased_at / purchasedAt datetime / Date yes --
provider_refund_fn / providerRefundFn (amount, txn_id, currency) -> any yes --
currency string no "usd"
provider string no "unknown"
now_fn / nowFn () -> datetime/Date no current UTC time

Returns a callable (Python) or async function (TypeScript) with signature (amount) -> result.


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 (your provider threw)

{"status": "error", "reason": "provider_error", "detail": "No such payment_intent: pi_xxx"}

Denial reasons

reason What it means Suggested message for users
refund_window_expired Purchase is older than the SKU's refund_window_days "The refund window for this order has closed."
amount_exceeds_limit Requested more than was originally paid "Refund amount exceeds the original charge."
amount_exceeds_remaining After partial refunds, not enough balance left "This order has already been partially refunded."
invalid_amount Amount is zero or negative "Please enter a valid refund amount."
provider_error Your provider call threw an exception "Refund could not be processed. Please contact support."

When status is "denied", your provider function was never called -- no money moved.


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.


What this library does NOT do

Prevent double refunds across HTTP requests The "remaining balance" check tracks partial refunds within a single makeRefundTool instance. Across separate requests, totalRefunded resets to 0. Your database (refunded_at or equivalent) is the source of truth.
Fetch order data You load SKU, amount, purchase date, and transaction ID from your database.
Replace your payment SDK It wraps your existing refund call. You still need Stripe / PayPal / etc.
Run on the client / frontend Server-side only. Your mobile or web app calls your backend; your backend runs this.
Tell your AI agent when to offer refunds The library enforces hard limits. Your agent's prompt encodes business rules. See the Integration Guide.

Troubleshooting

Symptom What to do
SKU 'x' not found in policy Add that SKU to your policy object or YAML file.
Cannot find module (TypeScript) Run npm install @mattmessinger/refund-guard in your project folder.
Forgot await (TypeScript) The refund callable is async: const r = await refund(10).
Every refund denied as amount_exceeds_limit You're probably passing minor units (cents) instead of major units (dollars). Most payment providers (Stripe, PayPal, Shopify) store amounts in cents -- divide by 100.
Policy file not found Pass an absolute path, or use an inline policy object instead.
refund_window_expired Expected if the purchase is older than the SKU's window.

Security model

Layer Role
Your payment provider Money + payment truth
Your app Order truth (SKU, ids, amounts, dates)
Agent / chat Untrusted -- only chooses refund amount inside the tool

Logging (Python)

import logging
logging.basicConfig()
logging.getLogger("refund_guard").setLevel(logging.INFO)

Develop / clone this repo

See CONTRIBUTING.md for full setup. Quick start:

git clone https://github.com/MattMessinger1/agentic_refund_guardrail.git
cd agentic_refund_guardrail
pip install -e ".[dev]" && pytest          # Python
cd packages/refund-guard-ts && npm ci && npm test  # TypeScript

Both languages run the same 13 test scenarios from contracts/parity/cases.json. If you change behavior in one language, the shared tests catch the drift.


FAQ

Why not trust the agent with transaction 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.

What do I tell my AI agent about refund policy? The library validates amounts and windows. Your agent's prompt should encode when to offer refunds. See the Integration Guide.

I'm integrating into a real app -- where do I start? Read the Integration Guide.

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.2.tar.gz (15.6 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.2-py3-none-any.whl (9.9 kB view details)

Uploaded Python 3

File details

Details for the file refund_guard-0.1.2.tar.gz.

File metadata

  • Download URL: refund_guard-0.1.2.tar.gz
  • Upload date:
  • Size: 15.6 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.2.tar.gz
Algorithm Hash digest
SHA256 fb8574f059ae878eda875828d59d47f6f460df9ced490c66a0ce09585b64846c
MD5 454b950892acf3f2cf0ebf9357d80b2e
BLAKE2b-256 0293e7c3ae05930b08f8fd23f0921da46b03d79c9120e1252e4f06d42843813a

See more details on using hashes here.

Provenance

The following attestation bundles were made for refund_guard-0.1.2.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.2-py3-none-any.whl.

File metadata

  • Download URL: refund_guard-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 9.9 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 d522f783c0105b606073f7c02aab730ea3980c780bdcada344b781fb4db24192
MD5 047511b41091f1badc17da2319427ce1
BLAKE2b-256 bb015e25654a6f2b50c2a61a9b3eec813f887aba6d310bbdfb6231cc44510ebe

See more details on using hashes here.

Provenance

The following attestation bundles were made for refund_guard-0.1.2-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