Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cspell/custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ androidx
Applebot
appname
ASGI
ATDRP
bazel
Blackhawk
Boku
Expand Down Expand Up @@ -168,6 +169,7 @@ superfences
Truelayer
Trulioo
udpa
unassessable
unmarshal
viewmodel
vulnz
Expand Down
50 changes: 50 additions & 0 deletions code/samples/python/scenarios/dispute/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# AP2 Sample: Resolving a Post-Transaction Dispute from the Mandate Chain

This sample shows what happens **after** an AP2 flow completes and the parties disagree.
It is stdlib-only and needs no API key: it reads a dispute record whose evidence is exactly
the artifacts a completed flow leaves behind and prints a determination under a published
policy (ATDRP-0.1).

## Scenario

A shopping agent completed a human-not-present purchase of 4 units under a signed Payment
Mandate; the merchant issued Success receipts; the order record shows 1 of 4 units
fulfilled and the merchant's own `estimated_delivery` window has passed with no
adjustment. The payer's agent opens a dispute on ground `performance`.

The resolver:

1. verifies the **authorization chain** the protocol defines — `payment_mandate.transaction_id`
and both receipts' `reference` bind to the closed checkout mandate's `checkout_hash`. A
chain that does not bind is a fraud/authorization question and is routed to the
network (`not_eligible: authorization_chain_broken`), which is where AP2 already sends it;
2. checks the payment receipt is `Success`;
3. applies a deterministic rule per line (moot by prior refund → uphold; charge above
mandate / execution after `exp` / mandate reuse → overturn; merchant cancel or units
unfulfilled after the merchant's own estimate → overturn the unfulfilled share;
instruction/cart conflict → recorded against the agent platform, merchant's charge
stands; otherwise uphold);
4. emits a determination with per-line outcomes and amounts, the five factors considered,
a signer-of-record slot, a fee disposition (loser pays), and a content-hash seal.

## Run

```sh
python code/samples/python/scenarios/dispute/resolve_dispute.py \
code/samples/python/scenarios/dispute/dispute_record.json --now 2025-10-24
```

Expected: line `line_item_456` is **overturned for 7500 minor units** (3 of 4 units
unfulfilled after the estimated delivery of 2025-10-16), fee paid by the merchant. Tamper
with `payment_mandate.transaction_id` and the same command returns
`not_eligible: authorization_chain_broken` with `route: network fraud process`.

## What this is and is not

- It is a demonstration that the AP2 trail is **sufficient evidence** for a neutral third
party to resolve non-fraud disputes without any data the protocol does not already emit.
- It names no provider and takes no position on who should run the resolver. The policy it
applies is published separately (ATDRP-0.1) and proposed to ACP as a `dispute_resolution`
extension.
- The seal here is a content hash; a production resolver anchors determinations on an
append-only chain with public verification.
35 changes: 35 additions & 0 deletions code/samples/python/scenarios/dispute/dispute_record.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"dispute_id": "dsp_01J9X0",
"policy_version": "ATDRP-0.1",
"claimant": "payer",
"checkout_hash": "u8Y2…checkout_hash…",
"payment_mandate": {
"vct": "mandate.payment.1",
"transaction_id": "u8Y2…checkout_hash…",
"payee": {"id": "merchant_42", "name": "Example Merchant"},
"payment_amount": {"currency": "USD", "amount": 10000},
"payment_instrument": {"type": "card", "token_ref": "redacted"},
"iat": 1760000000,
"exp": 1760086400
},
"checkout_receipt": {"status": "Success", "iss": "merchant_42", "iat": 1760000010, "reference": "u8Y2…checkout_hash…", "order_id": "ord_001"},
"payment_receipt": {"status": "Success", "iss": "psp_example", "iat": 1760000020, "reference": "u8Y2…checkout_hash…", "payment_id": "pay_001", "psp_confirmation_id": "psp_c_1", "network_confirmation_id": "net_c_1"},
"order": {
"id": "ord_001",
"checkout_session_id": "checkout_session_123",
"permalink_url": "https://merchant.example/orders/ord_001",
"status": "processing",
"estimated_delivery": {"earliest": "2025-10-14", "latest": "2025-10-16"},
"line_items": [
{"id": "line_item_456", "item": {"id": "item_789", "title": "Blue widget"}, "quantity": {"ordered": 4, "current": 4, "fulfilled": 1}, "total": 10000}
],
"fulfillments": [{"id": "ful_1", "type": "shipping", "line_items": [{"id": "line_item_456", "quantity": 1}], "events": [{"type": "shipped", "at": "2025-10-12T10:00:00Z"}]}],
"adjustments": []
},
"confirmed_cart": "4x Blue widget, $100.00 total, deliver by Oct 16",
"claim": {
"line_ids": ["line_item_456"],
"ground": "performance",
"statement": "Only 1 of 4 units delivered; estimated delivery window has passed with no adjustment."
}
}
Loading
Loading