Skip to main content

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

Project description

refund-guard

CI PyPI npm License: MIT

If your AI agent can call stripe.Refund.create(), it can try to refund anything -- wrong transaction, wrong amount, hallucinated order.

refund-guard adds one step: load a real order from your database, create a scoped refund tool, and give that tool to the agent. The agent can only refund that order, within your rules.

Install

pip install refund-guard            # Python
npm install @mattmessinger/refund-guard  # TypeScript / Node

Quickstart

from refund_guard import Refunds

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

order = get_order_from_db(order_id)   # YOUR database, not the agent

refund_tool = refunds.make_refund_tool(
    sku=order.sku,
    transaction_id=order.transaction_id,
    amount_paid_minor_units=order.amount_cents,  # library divides by 100
    purchased_at=order.purchased_at,
    refunded_at=order.refunded_at,               # None = not yet refunded
    provider_refund_fn=my_existing_refund_fn,     # your Stripe / PayPal / Shopify call
)

result = refund_tool(80.00)
# {"status": "approved", "refunded_amount": 80.0, ...}
# {"status": "denied", "reason": "refund_window_expired", ...}

That's it. Your provider function is only called if every check passes.

What it checks (before your refund function runs)

  • Already refunded -- if refunded_at is set, denied immediately
  • Refund window -- still within refund_window_days for that SKU
  • Positive amount -- must be > 0
  • Amount cap -- cannot exceed what was paid
  • Remaining balance -- handles partial refunds (can't refund $60 twice on a $100 order)

If any check fails, your provider function is never called -- no money moves.

TypeScript

import { Refunds, DENIAL_MESSAGES } 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,
  amountPaidMinorUnits: order.amountCents,
  purchasedAt: order.purchasedAt,
  refundedAt: order.refundedAt,
  providerRefundFn: myExistingRefundFn,
});

const result = await refund(80.0);  // async -- use await
const message = DENIAL_MESSAGES[result.reason as string] ?? "Refund processed.";

Both implementations follow the same behavior, enforced by shared parity tests.


API reference

Refunds(policy)

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

make_refund_tool(**opts) / makeRefundTool(opts)

Option Type Required Default
sku string yes --
transaction_id / transactionId string yes --
amount_paid / amountPaid number one of these --
amount_paid_minor_units / amountPaidMinorUnits int / number one of these --
purchased_at / purchasedAt datetime / Date yes --
provider_refund_fn / providerRefundFn (amount, txn_id, currency) -> any yes --
refunded_at / refundedAt datetime / Date or None/null no None
currency string no "usd"
provider string no "unknown"

Provide one of amount_paid (dollars) or amount_paid_minor_units (cents -- divided by 100 internally). Providing both raises an error.

DENIAL_MESSAGES

from refund_guard import DENIAL_MESSAGES
# {"refund_window_expired": "The refund window for this order has closed.", ...}

A dict / Record<string, string> mapping every denial reason to a user-facing message.


Denial reasons

reason Meaning
already_refunded refunded_at was set -- already refunded
refund_window_expired Purchase older than the SKU's window
amount_exceeds_limit Requested more than was paid
amount_exceeds_remaining Not enough balance after partial refunds
invalid_amount Zero or negative
provider_error Your provider threw an exception

Troubleshooting

Symptom Fix
Every refund denied as amount_exceeds_limit You're passing cents to amount_paid. Use amount_paid_minor_units instead.
Every refund denied as already_refunded You're passing a non-null refunded_at. This order is already refunded in your DB.
SKU 'x' not found in policy Add that SKU to your policy object or YAML file.
Forgot await (TypeScript) The callable is async: const r = await refund(10).

FAQ

Why not just trust the agent? Models hallucinate transaction IDs, mix up amounts, and retry incorrectly. This library binds the tool to one real order your server loaded.

Does this replace Stripe / PayPal / Shopify? No. It wraps your existing refund call with policy checks.

Do I need Python and TypeScript? No. Pick whichever your backend uses.

What does my provider function look like? (amount, transaction_id, currency) -> anything. Same for Stripe, PayPal, Shopify, or your own API.

What about double refunds across HTTP requests? Pass refunded_at from your database. The library denies immediately if it's set. If you don't pass it, you need to check it yourself.

What data does the agent control? Only the refund amount. SKU, transaction ID, amount paid, and purchase date all come from your database -- never from the agent.

Is this safe? The agent is untrusted. Your app provides order truth (SKU, IDs, amounts, dates). Your payment provider handles money. The agent only chooses how much to refund, inside the bounds you set.

How do I enable logging? (Python)

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

What do I tell my AI agent about refund policy? The library enforces hard limits (window, amount, balance). Your agent's system prompt should encode when to offer refunds. See the Integration Guide.

I'm wiring this into a real app with a database and Stripe. Where do I start? Read the Integration Guide -- a walkthrough based on actual production usage.


Contributing

See CONTRIBUTING.md for setup, tests, and PR guidelines.

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


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.2.1.tar.gz (13.5 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.2.1-py3-none-any.whl (9.3 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: refund_guard-0.2.1.tar.gz
  • Upload date:
  • Size: 13.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for refund_guard-0.2.1.tar.gz
Algorithm Hash digest
SHA256 a6f4142e1aea4746b3ea9a6380c5ae6e50ea5a0d1a6e7eca166b3f55df011bf0
MD5 6b2bac9d3d7a31dca14a3b71f2e37ea4
BLAKE2b-256 9ddab9de8c28b532abca3a7ee6a4a5ca3f72b6a98f3f0b5a8f122b35f131170d

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: refund_guard-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 9.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for refund_guard-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5eaf905c34b9ac89d410f1cca18485e8c1afb20453b78b1c780b537192c527c0
MD5 5f34e0658f3bec8068a59b3d1be0a6a8
BLAKE2b-256 ae312c9cd047bde6a5c6612d642e8a79aa5c104e5ead61fbd56b0b6f34a719b0

See more details on using hashes here.

Provenance

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