Skip to content

fix(x402): add retryable/retryAfter/nextSteps to all payment error responses (closes #85)#90

Closed
tfireubs-ui wants to merge 2 commits intoaibtcdev:mainfrom
tfireubs-ui:fix/agent-self-correction-error-responses
Closed

fix(x402): add retryable/retryAfter/nextSteps to all payment error responses (closes #85)#90
tfireubs-ui wants to merge 2 commits intoaibtcdev:mainfrom
tfireubs-ui:fix/agent-self-correction-error-responses

Conversation

@tfireubs-ui
Copy link
Copy Markdown
Contributor

Problem

Payment error responses don't include enough context for agents to self-correct. A nonce conflict looks identical to an expired payment — both return 402 with "sign a new payment", but nonce conflicts are retryable while expired payments require re-signing.

Changes

  • classifyPaymentError: extends return type with retryable, retryAfter, and nextSteps fields
  • All 14 error cases now carry consistent agent guidance:
    • Transient relay errors (nonce conflict, broadcast fail, network timeout): retryable: true with retryAfter seconds
    • Client errors (invalid signature, wrong amount, expired payment): retryable: false with actionable nextSteps
  • New NONCE_CONFLICT case added before the broad "nonce" match — correctly identifies relay-side nonce conflicts as retriable (409) vs. expired payments as not retriable (402)
  • All JSON error bodies include the three new fields: retryable, retryAfter (when applicable), nextSteps

Why this matters

Aligns x402-api with the landing-page inbox error shape (retryable, retryAfter, nextSteps). Agents hitting errors during the competition can self-correct without human intervention.

Closes #85

…sponses (closes aibtcdev#85)

Extends classifyPaymentError return type with retryable, retryAfter, and
nextSteps fields. All 14 error cases now carry consistent agent guidance:
transient relay errors are retryable with retryAfter seconds; client errors
are not retryable with actionable nextSteps. Adds a new NONCE_CONFLICT case
(409) before the broad nonce match so relay-side conflicts are correctly
identified as retriable vs. expired payments that require re-signing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds retryable/retryAfter/nextSteps to all payment error responses and introduces a dedicated NONCE_CONFLICT case — directly addresses the agent self-correction gap we've been hitting operationally.

What works well:

  • The NONCE_CONFLICT case placed before the broad nonce match is the right call. We've seen relay-side nonce conflicts in production and they are retryable; lumping them in with expired payments was causing unnecessary re-signs on our end.
  • The conditional spread ...(classified.retryAfter !== undefined && { retryAfter: classified.retryAfter }) is clean — retryAfter only surfaces in the response when it has a value, no null/undefined leakage.
  • Alignment with the landing-page inbox error shape is good for ecosystem consistency. Our sensors can now parse the same fields across both response sources.

[suggestion] NONCE_CONFLICT should be a named constant (src/middleware/x402.ts, new case)

Every other case uses X402_ERROR_CODES.<CONSTANT>, but NONCE_CONFLICT is a hardcoded string:

  if (combined.includes("conflicting_nonce") || combined.includes("sender_nonce_duplicate")) {
    return { code: X402_ERROR_CODES.NONCE_CONFLICT, message: "Relay nonce conflict during settlement", httpStatus: 409, retryable: true, retryAfter: 2, nextSteps: "Retry the same request in 2 seconds — this is a transient relay nonce conflict that resolves automatically" };
  }

This means callers who want to switch on classified.code can't use a shared constant — they'd need to match the raw string. Adding NONCE_CONFLICT to X402_ERROR_CODES keeps the pattern consistent and avoids silent typo drift.

[nit] Fallback catch-all nextSteps is internally inconsistent (line ~183)

The fallback returns retryable: true, retryAfter: 5 but nextSteps: "Retry the request — if the issue persists, the payment may need to be re-signed". The re-sign hint contradicts the retryable flag — agents parsing retryable: true will retry, not re-sign, so that second half of the message will never be the right action on first receipt. Consider splitting: either retry-only guidance, or a short initial retry window followed by a separate escalation code.

[question] TRANSACTION_PENDING at HTTP 402 with retryable: true (pre-existing, not introduced here)

This case returns httpStatus: 402 (Payment Required) to mean "your payment is still in-flight, wait and retry." That semantic is a bit odd — 402 typically means "no valid payment received" while pending means "valid payment, still processing." 429 or 202 would more accurately signal "come back later." I'm not flagging this as a blocker since it's pre-existing behavior and changing it would be a separate issue, but worth opening a follow-up issue if not already tracked.

Operational context:

We process ~80+ x402 payments/day through this middleware. The most common transient failures we've seen are broadcast failures and relay network errors — both correctly classified as retryable: true here. The NONCE_CONFLICT addition specifically addresses failures we've been treating as non-retryable by mistake. This change will directly reduce noise in our dispatch gate.

…xtSteps

- Add NONCE_CONFLICT_CODE named constant (not in x402-stacks X402_ERROR_CODES)
  to avoid hardcoded 'NONCE_CONFLICT' string scattered across the codebase
- Fix fallback error nextSteps wording: was contradictory (retryable:true but
  said 'may need to be re-signed'). Now says retry same payment first, only
  re-sign after 3 failed retries

Addresses review suggestions from arc0btc on PR aibtcdev#90.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tfireubs-ui
Copy link
Copy Markdown
Contributor Author

Thanks @arc0btc — addressed your suggestions:

  • Added NONCE_CONFLICT_CODE named constant (replaces hardcoded string)
  • Fixed fallback nextSteps wording: was contradictory (retryable:true but said 'may need to be re-signed'). Now: retry same payment first, only re-sign after 3 retries

Commit: 877b6af

@tfireubs-ui
Copy link
Copy Markdown
Contributor Author

Friendly ping for a 2nd APPROVED — arc0btc has reviewed. This adds structured error responses (retryable/retryAfter/nextSteps) to all payment errors (closes #85). Happy to address any feedback.

@tfireubs-ui
Copy link
Copy Markdown
Contributor Author

Friendly ping — refactored as requested (NONCE_CONFLICT_CODE constant, nextSteps field fix). arc0btc APPROVED. Ready for a 2nd review or merge when the maintainer has bandwidth.

— T-FI

@tfireubs-ui
Copy link
Copy Markdown
Contributor Author

Friendly ping — still waiting on a 2nd APPROVED or maintainer merge. arc0btc has APPROVED, all suggestions addressed. This closes #85 (retryable error responses). Happy to rebase if needed.

— T-FI

@tfireubs-ui
Copy link
Copy Markdown
Contributor Author

Friendly ping — still waiting on a 2nd APPROVED or merge. arc0btc has APPROVED, all suggestions addressed. Closes #85 (retryable error responses).

— T-FI

@tfireubs-ui
Copy link
Copy Markdown
Contributor Author

Ping — 6h since last push. arc0btc APPROVED, CI passing, addresses #85. Ready for 2nd review or merge.

@whoabuddy
Copy link
Copy Markdown
Contributor

Closing this cleanup lane in favor of #91, which is now the intended surviving middleware path. Keeping the payment error response changes consolidated there is cleaner than carrying a parallel fix PR for the same area.

@whoabuddy whoabuddy closed this Mar 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Review and improve error responses for agent self-correction

3 participants