Skip to content

[Bug]: x402 sample persists payment token as used before settlement is confirmed #308

Description

@giorgioroth

What happened?

In the x402 human-not-present sample at commit e1ea56d, merchant_agent_mcp.complete_checkout records the payment token as consumed and allocates an order before it calls the PSP, and does not restore that state when the call fails.

Observed ordering in code/samples/python/src/roles/merchant_agent_mcp/server.py:

Line Action
923 token_data['used'] = True
924–930 order_id allocated; amount_charged and currency recorded
946 _save_token_store(token_store) — persisted
984 POST to the x402 PSP at /settle-payment
1000 return {'error': 'PSP_call_failed', …} on exception

Between line 946 and line 1000 there is no reassignment of used, no deletion from the store, and no compensating write. The card flow beginning at line 1013 has the same ordering and raises ValueError on processor error, also without restoration.

Steps to reproduce:

  1. Run the x402 human-not-present scenario with BROADCAST_ON_CHAIN unset (default FALSE). Export a shared TEMP_DB_DIR so the roles share one state directory.
  2. Complete one purchase with all services reachable, to confirm the baseline. This succeeded, producing a receipt and an order id.
  3. Delete .temp-db/ap2_token_store.json.
  4. Stop the x402 PSP process listening on localhost:8084. Leave the other roles running.
  5. Repeat the flow: product query, $200 budget authorization, mandate approval, then the price-drop trigger setting the item to $199 with stock 10.

What I expected: either the token remains available for a further attempt, or the failure path records that settlement was not confirmed.

What happened: complete_checkout returned PSP_call_failed. No receipt and no transaction hash were produced. The token store nevertheless contained "used": true with an allocated order_id. Since complete_checkout begins by refusing a token already marked used, a further attempt presenting the same token would be refused as token_already_used.

Scope of the report. This concerns the sample implementation at one commit, in one configuration, with the settlement endpoint deliberately unavailable. It does not establish loss of funds: no transfer was attempted. It does not establish on-chain consumption of the EIP-3009 authorization: broadcast was disabled and no transaction was submitted. I did not test whether a retry in practice would present the same token or obtain a new payment credential.

Question: is this ordering intentional for the samples? If token consumption and order allocation are meant to precede settlement confirmation, is reconciliation expected to be the integrator's responsibility, or would a restoration step on the failure branch be in scope for the sample?

Related existing issues. I looked for prior coverage before filing. #44 requests that examples and guidance for handling failure states and enforcing mandate constraints be added; this report is about what the current sample does rather than what should be documented, so I filed it separately, but the two may belong together. #224 concerns proof-of-delivery before payment release, which is the same ordering question from the settlement side. If either already covers this, closing in favour of them is fine by me.

Full conditions, the baseline run and the limits of the observation: https://github.com/giorgioroth/AP2/blob/main/REPRO_X402_SETTLEMENT_FAILURE.md

Relevant log output

Merchant MCP · complete_checkout — Calling complete checkout via Merchant MCP

Error
PSP_call_failed
All connection attempts failed

--- token store after the failed run, truncated ---

{
  "x402_tok_4703483ae1954bb5a4e97a4fa62deed7": {
    "used": true,
    "order_id": "cf8e19d1-1631-4abd-9035-eee632f2a3f3",
    "expires_at": 1785351577,
    "currency": "USD",
    "checkout_jwt_hash": "14UvHy8FJkNG98TQ…",
    "open_checkout_hash": "QjZheS5cty8Q8mfz…"
  }
}

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions