ChainSafe / ChainSafe/canton-x402-sdk

feat(x402-express): verify rejections drop the facilitator's `extensions.detail`, and the middleware logs nothing

Open
#57 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
3
Forks
3
Avg merge
14h 2m
Merged PRs (30d)
3

Description

### Summary

When the facilitator rejects a payment at **verify**, `paymentRequired()` forwards only the coarse `invalidReason` enum to the client and logs nothing at all. The precise cause the facilitator computed — `extensions.detail`, plus any accompanying fields — is discarded. For rejections that share the `internal_error` bucket this makes the failure effectively undiagnosable from the merchant side.

The **settle** path already forwards its detail correctly. Verify is the only path that loses it, so this reads as an oversight rather than a design decision.

### Impact — a real incident

The mortgage example on DevNet failed every application with an opaque `credit bureau returned 402 / reason: "internal_error"`. Nothing appeared in the credit-bureau logs; the only visible signal was nginx logging a `502` from the backend.

The actual cause was the facilitator's anti-dust floor: `X402_MIN_AMOUNT_USD` defaults to `$2`, and the demo charges `0.005` CC. The facilitator *did* compute and return `extensions: { detail: "authorization_amount_too_low", minAmountMicroUsdc, amountMicroUsdc }` — which names the problem, the threshold, and the offered amount.

None of that reached the merchant. Diagnosing it took reading the facilitator's scheme source to work out which of the `internal_error` call sites could have fired. With the detail forwarded, it would have been one line of output.

### Root cause — three independent gaps

1. **The detail is dropped from the response.** `middleware.ts:152` passes only `verify.invalidReason` into `PaymentRejection`. PaymentRejection (middleware.ts:88-96) already accepts a third details?: string argument, and respond402 already emits it as details in the 402 body — verify just never populates it.
2. The package contains no logging whatsoever. A payment can be rejected for any reason and the merchant process stays silent. Same for @chainsafe/x402-client, which makes the facilitator call.

### Proposed fix

Forward the detail in middleware.ts:152, narrowing unknown → string the way the facilitator's own settle path does:
```ts
if (!verify.isValid) {
const detail = verify.extensions?.detail;
throw new PaymentRejection(
verify.invalidReason ?? "verify_failed",
fresh,
typeof detail === "string" ? detail : undefined,
);
}
```

Add opt-in logging.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in middleware.ts:152 and compare the verify rejection flow with PaymentRejection and respond402 at middleware.ts:88-96, then inspect the existing settle path for detail handling. Exercise a rejected verify payment and confirm the 402 response preserves a string detail while the new logging is opt-in and useful without making the merchant process noisy by default.

Written by the indexing model from the issue text.

Assessment

Tech stack
express, typescript
Domain
api, backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.