Repository navigation
fix(x402): derive payment-identifier with HMAC from account secret (#434) - #438
Merged
biwasxyz merged 2 commits intoOct 8, 2026
Merged
Conversation
…ibtcdev#434) Closes aibtcdev#434. Derives the payment-identifier using HMAC-SHA256 keyed by the account's privateKey when available, instead of plain SHA256 of the public transaction bytes. Key improvements: - Preserves cross-process and retry idempotency for the payer (skills aibtcdev#420). - Unguessable from public mempool transaction bytes, preventing proof-of-claim spoofing on servers that treat payment-identifier as proof of ownership. - Backwards compatible fallback to plain SHA256 when secretKey is omitted. - Honours server-issued payment identifier when returned in the response body or payment-response header extensions. - Pinned tests in x402-protocol.test.ts, x402-retry.test.ts, and x402.direct.test.ts.
… relay tracking id ahead of header echo - derivePaymentIdentifier now requires the payer's secret instead of silently falling back to a public SHA-256 of the tx bytes, which would reopen aibtcdev#434 for any future caller that omits it. - On the failure path, rank the canonical tracking hint above the payment-response header id: x402 v2 servers may echo the client's own payment-identifier there, which must not mask the relay-owned paymentId. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
biwasxyz
approved these changes
Oct 8, 2026
biwasxyz
left a comment
Contributor
There was a problem hiding this comment.
Reviewed. The HMAC derivation is the right fix for #434: deterministic for the payer (#420 re-sign test still passes), not computable from mempool bytes.
Pushed one fixup commit (dd18c18) on top:
derivePaymentIdentifiernow requires the secret. The plain-SHA256 fallback was unused by any caller and would silently reopen #434 for a future one (as sonic-mast flagged on the issue). Tests updated, including one that pins the identifier ≠ the public hash.- On the inbox failure path, the canonical tracking hint now ranks above the payment-response header id. x402 v2 servers may echo the client's own
payment-identifierextension there, and that echo shouldn't mask the relay-ownedpaymentIdused for status polling.
Typecheck clean; x402-protocol/x402-retry 16/16 and x402.direct 42/42 pass. Thanks!
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #434.
Why
Since #427,
derivePaymentIdentifier(txHex)generated the payment identifier frompay_+ SHA256(tx bytes)[:32]. While this solved #420 (cross-process and retry idempotency), signed transaction bytes become public once broadcast to the mempool.For resource servers (such as Vibewatch Stacks Vibe Index v2026.09.24+) that treat the
payment-identifieras proof of ownership for re-fetching paid responses after network hiccups or proxy 502s, a publicly-derivable identifier cannot prove claimant ownership. Consequently, retrying clients receive403 payment_identifier_requiredinstead of the stored paid response.What
src/lib/utils/x402-protocol.ts):derivePaymentIdentifier(txHex: string, secretKey?: string): stringsecretKey(e.g., accountprivateKey) is provided, derivespay_+HMAC-SHA256(secretKey, txHex)[:32].secretKeyis omitted.account.privateKeytoderivePaymentIdentifierincreateApiClient(src/lib/services/x402.service.ts) andexecuteInboxWithRetry(src/lib/utils/x402-retry.ts).extractServerIssuedPaymentIdentifier()inx402-retry.tsto honour server-issued identifiers returned in response envelopes (paymentId,payment_identifier,payment-identifier.info.id) orpayment-responseheader extensions on both success and error/retry paths.extensionstoSettlementResponseV2.src/lib/utils/x402-protocol.test.ts: Added tests for HMAC secret-keyed derivation determinism, mempool unguessability, and secret differentiation.src/lib/utils/x402-retry.test.ts: Added tests for server-issued payment identifier extraction across header extensions and response bodies.src/lib/services/x402.direct.test.ts: Verified re-signing test passes with HMAC-derived identifiers.bun run typecheck(tsc --noEmit) passes with 0 errors.Payout designation:
0x240C74A953Fc04Fe8cD713c68b53e94d9b449F8Cgewenbo.eth