ChainSafe / ChainSafe/canton-x402-sdk
feat(x402-express): verify rejections drop the facilitator's `extensions.detail`, and the middleware logs nothing
- 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