engineering

Agent Commerce Needs Evidence, Not a Reason Code

What would a dispute layer actually look like if you built one?

By Alex Kugell ·

Your AI agent booked a $450 hotel when you authorized up to $100. The payment went through. The hotel delivered the room. The card network has no reason code for what just happened, because nothing in the existing chargeback system covers it.

An agent booking the wrong hotel sounds like a reason to keep humans in the loop forever. But the same logic applies to every system that handles money at scale. Credit cards enabled fraud that cash never could, and we didn't go back to cash. We built fraud detection, chargeback codes, and liability frameworks. The value of the system justified the cost of building the safety infrastructure around it.

Agent commerce is the same bet. An agent that restocks your office supplies, pays your invoices, and books your travel without interrupting your afternoon is genuinely valuable. BLIK already processed one of these transactions in September. Visa and Mastercard announced a joint trust framework the same week. The question now is what the safety infrastructure looks like.

Every machine payment protocol being built right now handles the forward flow: challenge, pay, receipt, settle. Stripe's Machine Payments Protocol, x402, ACK-Pay. None of them handle the reverse flow. None of them answer: what happens when the agent was wrong?

The obvious response is to add a reason code. Visa could publish one tomorrow. But a reason code is a label for a dispute, not evidence of one.

A human dispute works because the bank calls you, you explain what happened, and someone makes a judgment. An agent dispute at scale can't rely on that phone call. You need evidence a third party can verify without calling either side.

That reframes the problem. Agent commerce doesn't need a new chargeback code. It needs an evidence architecture.

Two signed artifacts and a gap between them

Agent Commerce Kit's v2 proposal introduces two artifacts that create the opening for dispute evidence.

A grant is a short-lived JWT the owner signs, authorizing an agent to transact within a specific scope: this merchant, up to this amount, in this currency, before this time.

A receipt is a JWS the payment service signs after the transaction completes, and it carries an ack binding that ties it back to the grant by content hash.

So the authorization leg is signed. The action leg is signed. And there's a cryptographic reference connecting them. Both artifacts are JWTs you can verify with a stock JOSE library.

The other machine payment protocols don't have this structure.

MPP's receipt is a Payment-Receipt header that proves settlement happened, and contains a timestamp and transaction reference. It doesn't name which agent paid or reference any authorization scope.

x402's receipt proves a payment payload was accepted by a facilitator, but it doesn't carry a binding back to a grant or constraint set. Both protocols give you the action leg. Neither gives you the authorization leg or the link between them.

That binding is what makes dispute evidence possible. Without it, there's no second artifact to compare against.

What's missing is a machine-readable format for the mismatch between them. The grant says "up to $100 USDC." The receipt says "$450 USDC." A human can see the problem. A resolver processing thousands of disputes per hour needs a structured description it can verify mechanically.

What was authorized vs. what happened
Authorized
grant+jwt · signed by owner
Actual
receipt+jwt · signed by payment service
Amount10,000
constraints.maxAmount
4.5×
Amount45,000
amount
Recipientmerchant.com
aud
Recipientmerchant.com
recipient
Agentacme:shopper
sub
Agentacme:shopper
ack.agent
Timing15 min window
exp
Timingwithin window
iat < exp
dispute+jwtreason: scope-exceeded
signed by owner · embeds both artifacts · no callbacks
delta: constraints.maxAmount — authorized "10000" actual "45000" (USDC)

The evidence triple

A dispute evidence artifact is a JWT signed by the disputant — the person who issued the grant. It packages three things: the grant (what was authorized), the receipt (what happened), and a delta (which field didn't match, and by how much).

The constraint that shapes the entire design is callback-free verification. A resolver must be able to verify the dispute using only what's inside the artifact. No API calls to either party. No external state lookups.

That constraint forces three decisions.

The grant and receipt are embedded in full, not referenced by URL. A resolver doesn't fetch anything. The evidence is self-contained.

Both embedded artifacts are bound by content hash. The grant_ref is the SHA-256 of the grant's compact serialization. The receipt already carries this same hash in its ack.grant field. If someone forges a grant with narrower constraints than the one actually issued, the hash won't match the receipt's reference, and the resolver rejects.

The delta is structured, not narrative. Each entry names a field, the authorized value, and the actual value. A resolver extracts the named field from the embedded grant, extracts the corresponding value from the embedded receipt, and confirms they don't match. No interpretation required.

delta: [
  {
    field: "constraints.maxAmount",
    authorized: "100000000",  // 100 USDC (6-decimal subunits)
    actual: "450000000",      // 450 USDC
    currency: "USDC"
  }
]

The cost of this approach is expressiveness. A disputant who can't describe their complaint as a field mismatch between two artifacts can't use this protocol. They need a human arbitration layer. The protocol deliberately trades expressiveness for mechanical verifiability.

Four reason codes and the three that were cut

Traditional chargeback systems maintain dozens of reason codes across four categories. This protocol defines four, and the reason it's four is the mechanical verification constraint: every code must be verifiable by comparing fields in the embedded grant and receipt. No external data. No human judgment.

scope-exceeded — the action violated a grant constraint. The grant says max $100, the receipt says $450. The resolver extracts constraints.maxAmount from the grant, extracts amount from the receipt, compares. Mechanical.

grant-expired — the grant's expiration preceded the receipt's issuance. The resolver compares the grant's exp to the receipt's iat. Two timestamps, one comparison.

audience-mismatch — the payment went to a counterparty not named in the grant's aud. The grant says the agent can pay did:web:merchant-a.com. The receipt says the payment went to did:web:merchant-b.com. Field comparison.

unauthorized-agent — the receipt names an agent the owner didn't authorize. The grant's sub says one agent DID, the receipt's ack.agent says a different one.

Three codes were drafted and deliberately removed. Each one reveals a boundary in what machine-readable evidence can express.

Category mismatch was cut because receipts don't carry a category field. If your agent bought electronics when you authorized office supplies, there's no field in the receipt a resolver can extract and compare against the grant's constraints.category. You'd be right to dispute it, but a resolver can't verify it from the artifacts alone.

A future version of the receipt format that includes a category field could re-enable this code.

Revoked grant was cut because revocation is an external event. A grant gets revoked by removing the agent's key or adding the grant's jti to a blocklist. That revocation timestamp exists outside the grant and receipt, so a resolver would need to query external state. That breaks the self-contained property.

A signed revocation receipt could re-introduce this code, but that's a separate artifact.

No-grant was cut because without an ack binding, the receipt "attributes payment to no one." There's no grant to compare against. The disputant's evidence is the absence of any grant, which is a fact about the receipt alone, not a structured mismatch between two artifacts.

The registry is extensible — unrecognized codes don't cause rejection. The delta entries are self-describing, so a resolver can verify the mismatch even when it doesn't recognize the reason label.

Five steps a resolver walks

Verification follows a fixed sequence. A failure at any step rejects the entire dispute. There's no partial-evidence path.

Step 1: Evidence artifact. Decode the JWS. Verify typ is dispute+jwt. Resolve the disputant's identity and verify their signature. Check expiration.

Step 2: Grant. Verify the embedded grant is a valid JWT. Compute its SHA-256 and confirm it matches grant_ref. Confirm the grant's issuer matches the disputant.

Step 3: Receipt. Verify the embedded receipt is a valid JWS. Compute its SHA-256 and confirm it matches receipt_ref. Confirm the receipt's ack.agent matches the dispute's sub. Confirm the receipt's ack.grant matches grant_ref.

Step 3 is where forged disputes are caught. The receipt was signed by the payment service at transaction time, and it recorded which grant authorized the payment.

If the disputant embeds a forged grant with tighter constraints, the forged grant's hash won't match the ack.grant value the payment service recorded in the receipt. The resolver catches it at Step 3.

Step 4: Delta. For each entry, extract the named field from the grant, extract the corresponding value from the receipt, and verify the claimed authorized and actual values match what's in the artifacts. If the authorized and actual values are equal, there's no mismatch and the resolver rejects. The resolver's job is to confirm the values differ, not to judge whether the difference is important.

Step 5: Temporal ordering. The grant was issued before the receipt. For non-expiry disputes, the grant was still valid when the receipt was issued. The dispute was filed after the receipt. Four timestamps, three comparisons.

The whole thing runs with jose. Here's what the dispute creation and the first verification step look like:

import { SignJWT, jwtVerify } from "jose"
 
const dispute = await new SignJWT({
  reason: "scope-exceeded",
  grant_ref: grantRef,
  receipt_ref: receiptRef,
  evidence: { grant, receipt, delta },
})
  .setProtectedHeader({ alg: "EdDSA", typ: "dispute+jwt", kid: ownerKid })
  .setIssuer(ownerDid)
  .setSubject(agentDid)
  .setIssuedAt()
  .setExpirationTime("90d")
  .sign(owner.privateKey)

And verification:

const { payload } = await jwtVerify(dispute, owner.publicKey, {
  typ: "dispute+jwt",
})
 
const computedRef = bufToBase64url(
  new Uint8Array(await crypto.subtle.digest(
    "SHA-256", new TextEncoder().encode(payload.evidence.grant)
  ))
)
// computedRef must match payload.grant_ref

No new cryptographic primitives. The same key resolution and signature verification that ACK core already uses.

What this doesn't solve

Dispute evidence produces a verified mismatch, not a resolution. Whether a scope-exceeded dispute leads to a refund, a credit, or an arbitration hearing is a business decision above the protocol. The protocol is explicit about this: evidence, not verdicts.

Fund reversal is out of scope. ACK receipts are attestations of payment, not settlement instructions. Reversing funds is the payment network's job.

Multi-hop delegation is out of scope. If Agent A delegates to Agent B, the grant chain deepens. This proposal assumes a single agent-to-owner grant. A delegation-aware dispute layer needs the chain-verification extension first.

Intent capture is out of scope, and this is the hardest exclusion to accept. A grant encodes authorization scope — max amount, currency, recipient, expiry. It doesn't encode conversational intent. "Buy me hand cream" becoming "booked a hotel" is a real dispute that can't be expressed as a field mismatch between structured artifacts.

Capturing and verifying natural-language intent is a harder problem. Making it a dependency would have blocked the parts that are solvable now.

Evidence expires. The proposal recommends a 90-day window from the receipt's issuance, matching common chargeback windows. A legitimate dispute filed on day 91 is unverifiable by compliant resolvers.

What the other protocols would need

The evidence triple pattern (authorized artifact, action artifact, structured delta) isn't ACK-specific. It works on any protocol that has two signed artifacts and a cryptographic link between them. ACK is the worked example because it already has all three prerequisites. MPP and x402 don't.

MPP would need two additions. First, an authorization scope artifact — something equivalent to a grant that carries constraints like max amount, allowed recipients, and expiry. Second, a binding in the Payment-Receipt header that references which scope covered this payment. Today MPP's receipt is a settlement proof with no link back to what the user authorized. Add those two pieces and the same delta-based dispute evidence applies.

x402 got the closest to acknowledging the problem. Its offer-and-receipt extension defines signed offers and signed receipts, and the spec explicitly names "dispute evidence" and "dispute workflows, including scenarios involving automated purchasers (agents)" as downstream use cases. Then it stops. The evidence building blocks exist, but the receipt doesn't carry a reference to any constraint set the user defined. A grant-like artifact bound into the receipt by content hash would give a resolver the two legs it needs.

Dispute evidence readiness by protocol
ACK
MPP
x402
Action artifact
receipt / proof of payment
ships
ships
ships
Authorization artifact
grant / scoped permission
ships
needs work
needs work
Receipt → authorization binding
cryptographic link between them
ships
needs work
needs work
Dispute evidence
structured delta + verification
proposed
needs work
needs work

The pattern is the same across all three. The prerequisites are not. ACK's v2 grant model and receipt ack binding put it closest to supporting dispute evidence today. MPP and x402 would need to add authorization artifacts and receipt bindings before the evidence layer can work.

Where this sits

This is a proposal draft, not a shipped spec. It depends on v2 grants and the receipt ack binding from the ACK-ID + ACK-Pay v2 RFC. The claim shapes could still change.

But the architecture question it answers is already live. Agent-initiated transactions are shipping on real rails. BLIK processed one in September. Visa and Mastercard announced a joint trust framework the same week. The authorization protocols are being built. The dispute evidence layer that should accompany them hasn't started, in any of them.

I filed the dispute evidence question in all three protocols: MPP, x402, and ACK. The ACK proposal is the most developed because ACK already has the grant-to-receipt binding. The other two would need to add it first.

The first spec to define machine-verifiable dispute evidence for agent commerce will set the vocabulary for every resolver, arbitration service, and compliance team that follows. Four reason codes and a five-step checklist is a starting point, not the final answer. But it's the shape of the answer: structured evidence, mechanical verification, no callbacks, and a stated cost for everything it can't express.

Sources

Built by Trio, a fintech-native engineering partner helping teams build the next generation of financial technology and infrastructure.

Subscribe to Ledger Drift for high-signal insights into how modern fintech is built, from systems to code to teams.

Keep reading

analysisThe Settlement Stack Just Became Block ValidatorsDTCC, BlackRock, Visa, Mastercard, and ICE are now validating blocks on a Circle blockchain. That's not crypto adoption....
analysisAgent Commerce Has No Chargeback CodeWhen an AI agent buys the wrong hotel, the chargeback system has no reason code for what just happened.
fintechBNPL Is Becoming InfrastructurePay-in-four started as a consumer brand. It's ending up as plumbing inside someone else's checkout.
View more ›