Keywords: chargeback response, dispute management, representment, evidence package, reason codes, fraud chargeback, product not received, subscription canceled dispute, credit not processed, Visa 13.1, Mastercard 4855, Stripe dispute evidence, Razorpay dispute contest
Give it a dispute (reason code, amount, transaction context) and the evidence you have. It tells you what is missing, drafts the rebuttal narrative, and assembles a submission-ready evidence package. It never submits anything in v0.
Install, run one command on the sample data, read the recommendation and the drafted response. No API key.
git clone https://github.com/pandeyvishwas51-oss/dispute-agent.git
cd dispute-agent
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
dispute-agent demoSame flow on the example file:
dispute-agent triage examples/case.json
python examples/run_case.pyThat is the rule-based engine: score, gaps, fight or accept, and a template narrative. Set a model and a key, run the same command, and the optional model layer writes the rebuttal instead.
| Provider | LLM_MODEL |
Env |
|---|---|---|
| OpenAI | gpt-4o |
OPENAI_API_KEY |
| Anthropic | claude-opus-5 |
ANTHROPIC_API_KEY |
gemini/gemini-2.0-flash |
GEMINI_API_KEY |
|
| OpenRouter | openrouter/openai/gpt-4o |
OPENROUTER_API_KEY |
| Local Ollama | ollama/llama3.2 |
none (Ollama on localhost) |
| vLLM | hosted_vllm/<model-name> |
none (OPENAI_API_BASE to the server) |
# OpenAI
export LLM_MODEL=gpt-4o
export OPENAI_API_KEY=sk-your-key
# Anthropic
export LLM_MODEL=claude-opus-5
export ANTHROPIC_API_KEY=sk-ant-your-key
# Local Ollama
export LLM_MODEL=ollama/llama3.2Default LLM_MODEL is gpt-4o-mini when you set a key and skip the model name. With no key and no local model, the agent stays on templates.
dispute-agent is a small Python library and CLI for chargeback response and dispute management. Card networks classify a claim with a reason code. Each code expects a different evidence set. This project maps those codes, scores what you already have, lists the gaps, and drafts the representment narrative.
v0 is read-only. Your own code talks to the gateway when you are ready to contest.
| Piece | Role |
|---|---|
reasons.py |
Reason-code to required-evidence map. Data only. Sourced from public network tables. |
CaseEngine |
Scores evidence, lists gaps, recommends accept or fight. No API key. |
DisputeAgent |
Optional model loop (any LLM via litellm, LLM_MODEL) for the rebuttal. Falls back to templates with no key. |
| Loaders | Generic JSON, a Razorpay dispute shape, a Stripe dispute shape, and a simulated generator. No live calls. |
| CLI | dispute-agent triage and dispute-agent demo print the plan, gaps, and draft. |
How do I respond to a chargeback?
Load the dispute as a Dispute (reason code, amount, transaction, evidence already on file) and call CaseEngine().plan(...). The engine maps the reason code (for example Visa 13.1 / product not received) to the evidence that code needs, scores coverage from 0 to 1, lists gaps, and recommends fight or accept. DisputeAgent().triage(...) adds a factual rebuttal narrative. Run dispute-agent demo to see this on generated cases.
Does this submit the representment to Visa, Mastercard, Stripe, or Razorpay?
No. v0 never contests a dispute, never uploads documents, and never calls a payment API. ResponsePackage.submitted is always False. Take the draft and submit it from your own system.
Can I use this with Razorpay or Stripe?
Yes, as data. Map a dispute object with load_case(..., fmt="razorpay") or fmt="stripe". Field names follow the public docs (Razorpay contest evidence keys, Stripe Dispute evidence hash). The loaders do not use API keys and do not open a network socket.
Gateway dashboards already collect evidence. Chargeback vendors already file representment. Payment agent studios sell dispute management as a product: triage the claim, gather proof, draft a response. This library is the open, testable core of that job. You can unit-test the reason map and the score with no API key, then plug the draft into whatever submits.
| Card network portal | Gateway dispute UI | Hosted chargeback vendor | This library | |
|---|---|---|---|---|
| Works across gateways | One network | That gateway | That vendor's processors | Yes, through loaders |
| Reason-code evidence map you can test offline | Policy PDFs | In-product tips | In-product | Yes (reasons.py, no key) |
| Gap list and accept-or-fight score | You decide | Partial | Vendor model | Yes (caseengine.py) |
| Rebuttal narrative | You write it | Text box | Vendor draft | Drafted here, not sent |
| Submits representment in v0 | You click submit | You click submit | Vendor submits | Never |
| Needs an API key to demo | Yes | Yes | Yes | No |
Tradeoffs: this will not pull live disputes, will not attach files to a gateway, and will not track win rates. The Stripe and Razorpay loaders are shape mappers from public JSON. If you already like a vendor's filing workflow, keep it and use this as the checklist.
Everything below is what the code does, not a wish list.
-
Input. A
Disputeis a reason code, an amount in minor units (paise, cents), optionalTransactioncontext, and a list ofEvidenceitems. Loaders accept generic JSON, a StripeDisputeobject, or a Razorpay dispute entity (including a webhook envelope). -
Normalize the reason.
normalize_reasonmaps gateway strings and network codes onto one of six categories:fraudulent,product_not_received,product_unacceptable,duplicate,subscription_canceled,credit_not_processed. Unknown strings fall back togeneral. Aliases include Visa 10.4 / 13.1 / 13.3 / 12.6.1 / 13.2 / 13.6, Mastercard 4837 / 4855 / 4853 / 4834 / 4841 / 4860, and Stripe'sreasonenum. -
Required evidence.
reasons.pyholds, for each category, weightedEvidenceReqrows (kind, weight, critical flag, alternatives, how to obtain) plus a short sourced comment pointing at the public Stripe reason-code table, which restates Visa / Mastercard / Amex defense examples. -
Score.
score_evidencemarks a requirement satisfied if that kind is present or if an alternative is (digital access logs can stand in for shipping proof on product-not-received). Score is earned weight over total weight. Critical misses are listed separately. Digital goods flip shipping off the critical list and putaccess_activity_logon it. -
Accept or fight. Default:
fightwhen score is at least 0.55 and no critical item is missing; otherwiseaccept. Strength isstrongat 0.80,moderateat 0.55, elseweak.EngineConfigcan change the thresholds. -
Strongest argument. The engine walks a per-category priority list (3-D Secure before IP on fraud, POD before tracking on product-not-received, refund confirmation before policy on credit-not-processed) and writes one factual sentence from the evidence content.
-
Copy.
draft_narrativefills a template: recommendation, network codes, transaction line, merchant position, evidence index, gaps. The last line states that the package has not been submitted. -
Optional model.
DisputeAgentalways runs the case engine first. If a provider key is set, orLLM_MODELpoints at Ollama or vLLM, it starts a tool-use loop (get_case,get_reason_spec,score_case,list_gaps,submit_package) through litellm. Arefusalfinish reason, a missing SDK, a missing key,--no-agent,DISPUTE_AGENT_DISABLE_AGENT=1, or any exception falls back to the templates.submit_packageonly fills the in-memoryResponsePackage. It does not call a gateway. -
Output. CLI prints the plan, gaps, argument, and draft.
submittedis alwaysFalse.
dispute JSON -> normalize reason -> required evidence
| |
v v
CasePlan (score, gaps + argument
fight | accept)
|
v
ResponsePackage + drafted narrative
|
v
your integration submits (not v0)
Python 3.11 or newer.
git clone https://github.com/pandeyvishwas51-oss/dispute-agent.git
cd dispute-agent
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -qRuntime dependency: litellm (imported only if a key or local model is set). Optional extra: pydantic (pip install -e ".[pydantic]") if you want it in the same environment for your own wrappers. The case engine uses dataclasses and the standard library.
dispute-agent demo
dispute-agent triage examples/case.json
dispute-agent --json --no-agent triage examples/case.jsondispute-agent demo covers a fraud case with 3-D Secure, an empty fraud file, a delivered physical order, a product-not-received with no proof, a digital download, a weak product-unacceptable claim, an explained duplicate, a subscription with post-cancel usage, a credit that already refunded, and a credit with no policy on file.
from dispute_agent import CaseEngine, Dispute, Evidence, Transaction
from datetime import datetime, timezone
txn = Transaction(
id="pay_keyboard",
amount=249900,
currency="INR",
occurred_at=datetime(2026, 8, 1, tzinfo=timezone.utc),
product_description="Mechanical keyboard",
)
dispute = Dispute(
id="disp_1",
reason_code="product_not_received",
amount=249900,
currency="INR",
network_reason_code="13.1",
transaction=txn,
evidence=[
Evidence(kind="shipping_documentation", content="BlueDart POD signed 2026-08-03"),
Evidence(kind="shipping_tracking_number", content="BD123456789IN"),
Evidence(kind="receipt", content="INV-2044"),
],
)
plan = CaseEngine().plan(dispute)
print(plan.recommendation, plan.score, plan.rationale)from dispute_agent import DisputeAgent
result = DisputeAgent(force_templates=True).triage(dispute)
print(result.recommendation)
print(result.narrative)
print(result.submitted) # FalseSet LLM_MODEL and the matching provider key if you want the model loop to write the narrative instead of the templates. Use --no-agent on the CLI to force templates even when a key is present.
from dispute_agent import load_case, CaseEngine
stripe_case = load_case("examples/stripe_dispute.json", fmt="stripe")
razorpay_case = load_case("examples/razorpay_dispute.json", fmt="razorpay")
print(CaseEngine().plan(stripe_case).recommendation)
print(CaseEngine().plan(razorpay_case).gaps)Neither loader reads STRIPE_SECRET_KEY or Razorpay keys. Put test values in a git-ignored .env only if your own integration needs them later. This package will not use them.
- Run dispute-agent when a dispute arrives.
- If
recommendation == "accept", refund or accept in the gateway yourself, using your own rules. - If
recommendation == "fight", attach the listed evidence and the narrative through the gateway contest API yourself. - Watch the respond-by deadline (
due_by). Missing it loses by default. - Do not treat the score as a guarantee. Issuers decide.
Python 3.11+. Tests run on 3.11, 3.12, and 3.13 in CI.
No. The reason map, case engine, templates, loaders, and simulated demo run without one. A key is only for the optional tool-use loop, and it can be OpenAI, Anthropic, Google, OpenRouter, or nothing if you use Ollama.
Whatever you set in LLM_MODEL. The default is gpt-4o-mini (dispute_agent.DEFAULT_MODEL). A refusal finish reason is treated as a fallback to templates, not as a crash.
Shipping proof (or a digital access log) is critical for that reason code. A receipt alone does not show the goods arrived. Attach tracking or a POD, or accept.
For product_not_received on transaction.is_digital=True, access_activity_log is critical and shipping paper is not. Login or download logs are the delivery proof.
Write a function that maps the provider payload onto Dispute / Evidence / Transaction. Keep contest HTTP calls out of that mapping.
Not in v0. The package lists kinds and content strings. You upload through the gateway's documents API.
Yes. CI unsets provider keys and sets DISPUTE_AGENT_DISABLE_AGENT=1. The litellm layer is mocked. Stripe and Razorpay tests cover payload mapping only.
pip install -e ".[dev]"
pytest -qMIT. Free for commercial and personal use.