Skip to content

fix(x402): derive payment-identifier with HMAC from account secret (#434) - #438

Merged
biwasxyz merged 2 commits into
aibtcdev:mainfrom
gewenbo888:fix/x402-payment-identifier-hmac
Oct 8, 2026
Merged

biwasxyz merged 2 commits into
aibtcdev:mainfrom
gewenbo888:fix/x402-payment-identifier-hmac

Conversation

@gewenbo888

Copy link
Copy Markdown
Contributor

Closes #434.

Why

Since #427, derivePaymentIdentifier(txHex) generated the payment identifier from pay_ + 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-identifier as 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 receive 403 payment_identifier_required instead of the stored paid response.

What

  1. HMAC-Keyed Derivation (src/lib/utils/x402-protocol.ts):
  2. Key Injection:
    • Passed account.privateKey to derivePaymentIdentifier in createApiClient (src/lib/services/x402.service.ts) and executeInboxWithRetry (src/lib/utils/x402-retry.ts).
  3. Honour Server-Issued Identifiers:
    • Added extractServerIssuedPaymentIdentifier() in x402-retry.ts to honour server-issued identifiers returned in response envelopes (paymentId, payment_identifier, payment-identifier.info.id) or payment-response header extensions on both success and error/retry paths.
    • Added optional extensions to SettlementResponseV2.
  4. Comprehensive Tests:
    • 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:

  • EVM: 0x240C74A953Fc04Fe8cD713c68b53e94d9b449F8C
  • ENS: gewenbo.eth

…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 biwasxyz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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:

  1. derivePaymentIdentifier now 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.
  2. 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-identifier extension there, and that echo shouldn't mask the relay-owned paymentId used for status polling.

Typecheck clean; x402-protocol/x402-retry 16/16 and x402.direct 42/42 pass. Thanks!

@biwasxyz
biwasxyz merged commit 9e75572 into aibtcdev:main Oct 8, 2026
2 checks passed
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.

x402: tx-derived payment-identifier is computable from public tx bytes, so it can't prove the claimant on retry

2 participants