Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Chargeback response and dispute evidence packages for representment

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.

CI License: MIT Python 3.11+

Use it in two minutes

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 demo

Same flow on the example file:

dispute-agent triage examples/case.json
python examples/run_case.py

That 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
Google 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.2

Default 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.

What is dispute-agent?

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.

Direct answer

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.

Why this exists

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.

How it works

Everything below is what the code does, not a wish list.

  1. Input. A Dispute is a reason code, an amount in minor units (paise, cents), optional Transaction context, and a list of Evidence items. Loaders accept generic JSON, a Stripe Dispute object, or a Razorpay dispute entity (including a webhook envelope).

  2. Normalize the reason. normalize_reason maps 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 to general. 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's reason enum.

  3. Required evidence. reasons.py holds, for each category, weighted EvidenceReq rows (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.

  4. Score. score_evidence marks 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 put access_activity_log on it.

  5. Accept or fight. Default: fight when score is at least 0.55 and no critical item is missing; otherwise accept. Strength is strong at 0.80, moderate at 0.55, else weak. EngineConfig can change the thresholds.

  6. 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.

  7. Copy. draft_narrative fills a template: recommendation, network codes, transaction line, merchant position, evidence index, gaps. The last line states that the package has not been submitted.

  8. Optional model. DisputeAgent always runs the case engine first. If a provider key is set, or LLM_MODEL points at Ollama or vLLM, it starts a tool-use loop (get_case, get_reason_spec, score_case, list_gaps, submit_package) through litellm. A refusal finish reason, a missing SDK, a missing key, --no-agent, DISPUTE_AGENT_DISABLE_AGENT=1, or any exception falls back to the templates. submit_package only fills the in-memory ResponsePackage. It does not call a gateway.

  9. Output. CLI prints the plan, gaps, argument, and draft. submitted is always False.

dispute JSON  ->  normalize reason  ->  required evidence
                         |                    |
                         v                    v
                   CasePlan (score,     gaps + argument
                   fight | accept)
                         |
                         v
              ResponsePackage + drafted narrative
                         |
                         v
              your integration submits (not v0)

Install

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 -q

Runtime 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.

Quick start

Simulated demo (no keys)

dispute-agent demo
dispute-agent triage examples/case.json
dispute-agent --json --no-agent triage examples/case.json

dispute-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.

Python, engine only

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)

Draft a package (still not submitted)

from dispute_agent import DisputeAgent

result = DisputeAgent(force_templates=True).triage(dispute)
print(result.recommendation)
print(result.narrative)
print(result.submitted)  # False

Set 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.

Map a Stripe or Razorpay payload

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.

What your integration should do (not shipped)

  1. Run dispute-agent when a dispute arrives.
  2. If recommendation == "accept", refund or accept in the gateway yourself, using your own rules.
  3. If recommendation == "fight", attach the listed evidence and the narrative through the gateway contest API yourself.
  4. Watch the respond-by deadline (due_by). Missing it loses by default.
  5. Do not treat the score as a guarantee. Issuers decide.

FAQ

What Python versions are supported?

Python 3.11+. Tests run on 3.11, 3.12, and 3.13 in CI.

Do I need an Anthropic key?

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.

Which model does the optional loop use?

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.

Why did a product-not-received case with only a receipt recommend accept?

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.

Why can a digital order fight without a tracking number?

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.

How do I add another gateway?

Write a function that maps the provider payload onto Dispute / Evidence / Transaction. Keep contest HTTP calls out of that mapping.

Can this upload documents to a dispute?

Not in v0. The package lists kinds and content strings. You upload through the gateway's documents API.

Are tests offline?

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.

How do I run tests?

pip install -e ".[dev]"
pytest -q

License

MIT. Free for commercial and personal use.

About

Open-source agent for payment disputes and chargebacks. Triages a case by reason code, lists the missing evidence, and drafts the representment. Deterministic case engine plus a Claude agent for the argument. Gateway-agnostic (Razorpay, Stripe).

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages