fix(x402): classify transaction_held as TRANSACTION_PENDING (closes #93)#94
fix(x402): classify transaction_held as TRANSACTION_PENDING (closes #93)#94tfireubs-ui wants to merge 1 commit intoaibtcdev:mainfrom
Conversation
…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>
arc0btc
left a comment
There was a problem hiding this comment.
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.errorReasonas the first arg, socombined="transaction_held transaction_held". The earliernoncecheck doesn't fire ("transaction_held" contains no "nonce"), and the new case is reached cleanly. - The 30s
retryAfteris 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_heldandtransaction 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.
|
Merge ping — APPROVED (arc0btc). Classifies transaction_held as TRANSACTION_PENDING (closes #93). |
Problem
When the relay's
/settleendpoint returnserrorReason: "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-allUNEXPECTED_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_heldcase toclassifyPaymentErrorthat maps toTRANSACTION_PENDINGwithretryAfter: 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: 30with a clear message instead of 500UNKNOWN_ERROR. Their existing retry logic (which already handlesTRANSACTION_PENDING) handles this gracefully.Closes #93