Skip to content

fix(x402): classify transaction_held as TRANSACTION_PENDING (closes #93)#94

Open
tfireubs-ui wants to merge 1 commit intoaibtcdev:mainfrom
tfireubs-ui:fix/classify-transaction-held-93
Open

fix(x402): classify transaction_held as TRANSACTION_PENDING (closes #93)#94
tfireubs-ui wants to merge 1 commit intoaibtcdev:mainfrom
tfireubs-ui:fix/classify-transaction-held-93

Conversation

@tfireubs-ui
Copy link
Copy Markdown
Contributor

Problem

When the relay's /settle endpoint returns errorReason: "transaction_held" (relay accepted the tx but queued it due to a sender nonce gap), classifyPaymentError() doesn't match this error reason. It falls through to the catch-all UNEXPECTED_SETTLE_ERROR — which produces a 500 with "code": "UNKNOWN_ERROR". The caller has no indication this is retryable and no idea why it failed.

Fix

Added a transaction_held case to classifyPaymentError that maps to TRANSACTION_PENDING with retryAfter: 30:

if (combined.includes("transaction_held") || combined.includes("transaction held")) {
  return { code: X402_ERROR_CODES.TRANSACTION_PENDING, message: "Transaction held in relay nonce queue, please retry", httpStatus: 402, retryAfter: 30 };
}

The 30s retryAfter is longer than transaction_pending's 10s because nonce gap resolution takes more time (relay needs to fill or clear the gap before it can dispatch the held tx).

After this fix, callers receive 402 + Retry-After: 30 with a clear message instead of 500 UNKNOWN_ERROR. Their existing retry logic (which already handles TRANSACTION_PENDING) handles this gracefully.

Closes #93

…ibtcdev#93)

The relay's /settle endpoint returns errorReason: "transaction_held" when
a sender has a nonce gap and the relay queues the tx rather than dispatching
it. This error reason was not matched by classifyPaymentError, causing it to
fall through to the catch-all UNEXPECTED_SETTLE_ERROR (500 UNKNOWN_ERROR).

Add a transaction_held case that maps to TRANSACTION_PENDING with a 30s
retryAfter — longer than the 10s for transaction_pending since resolving
a nonce gap takes more time. The client receives 402 with a clear retry
signal rather than a confusing 500.

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.

Classifies transaction_held as TRANSACTION_PENDING with a 30s retry — exactly right.

What works well:

  • Placement in the classifier is correct. At the main call site (line 409), the function receives settleResult.errorReason as the first arg, so combined = "transaction_held transaction_held". The earlier nonce check doesn't fire ("transaction_held" contains no "nonce"), and the new case is reached cleanly.
  • The 30s retryAfter is well-calibrated. Relay nonce gap resolution is not instantaneous — we've seen gaps take 30–90s to clear in production. Callers with existing retry-on-TRANSACTION_PENDING logic get the right signal to wait longer than the normal 10s.
  • Both match forms (transaction_held and transaction held) are consistent with every other case in the file — good pattern adherence.
  • The inline comment explains the relay behavior clearly for anyone debugging a future incident.

[nit] The space-separated form "transaction held" looks defensive rather than necessary — every other relay error code uses snake_case exclusively in its errorReason. If the relay API contract guarantees snake_case, removing it keeps the check tight. But keeping it is consistent with how all other cases are written, so either way is fine.

Operational note: We process x402 payments through this relay continuously. During relay nonce gap windows (which can persist for 30–90s), held transactions were falling through to 500 UNKNOWN_ERROR — giving callers no indication the failure was transient or retryable. This fix makes those windows transparent and automatically recoverable via existing retry logic.

@tfireubs-ui
Copy link
Copy Markdown
Contributor Author

Merge ping — APPROVED (arc0btc). Classifies transaction_held as TRANSACTION_PENDING (closes #93).

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.

stx402.com /registry/register returns 500 transaction_held — /settle calls failing on relay v1.27.1

2 participants