Skip to content

feat: add credential validation and settlement APIs#597

Open
brendanjryan wants to merge 2 commits into
mainfrom
brendanjryan/validate-settle-credentials
Open

feat: add credential validation and settlement APIs#597
brendanjryan wants to merge 2 commits into
mainfrom
brendanjryan/validate-settle-credentials

Conversation

@brendanjryan

@brendanjryan brendanjryan commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Some integrations need a way to inspect whether a payment credential currently matches a challenge before deciding to accept it and ultimately settle.

Validation should not reserve replay state, sign fee-payer transactions, broadcast payments, or otherwise mutate payment state. Settlement remains the authoritative path and re-runs validation before consuming the credential.

This ~somewhat mirrors the x402 verify flow, and also has the benefit of making this code more explicit.

Summary

  • Added validateCredential for non-mutating credential pre-checks.
  • Added settleCredential for explicit credential consumption and settlement.
  • Kept verifyCredential as the backwards-compatible settlement path.
  • Added Tempo charge validation for hash, proof, and pull transaction credentials.

Motivation

Key design considerations

  • Existing integrations can keep using verifyCredential and receive the same receipt-oriented behavior.
  • New integrations can split pre-checks from payment acceptance:
const validation = await mppx.validateCredential(credential, {
  request: { amount: '0.01' },
})

await riskEngine.approve({
  amount: validation.request.amount,
  method: validation.method,
  source: validation.source,
})

const receipt = await mppx.settleCredential(credential, {
  request: { amount: '0.01' },
})
  • Method authors can opt into the split lifecycle while retaining legacy verify support:
const charge = Method.toServer(Methods.charge, {
  async validate({ credential, request }) {
    return {
      challenge: credential.challenge,
      credential,
      details: { mode: 'pull' },
      intent: 'charge',
      method: 'tempo',
      request,
      source: credential.source,
    }
  },
  async settle(context) {
    return settlePayment(context)
  },
  async verify(context) {
    return settlePayment(context)
  },
})
  • Settlement paths call validation again for methods that provide both hooks, so callers cannot rely on stale pre-check results.

@pkg-pr-new

pkg-pr-new Bot commented Jun 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/mppx@597

commit: e15a622

@brendanjryan brendanjryan force-pushed the brendanjryan/validate-settle-credentials branch 2 times, most recently from effcf69 to 0e08e87 Compare June 30, 2026 00:46
@brendanjryan brendanjryan marked this pull request as ready for review June 30, 2026 00:51
@brendanjryan brendanjryan force-pushed the brendanjryan/validate-settle-credentials branch 3 times, most recently from fdef078 to e4a6b90 Compare June 30, 2026 01:02
@brendanjryan brendanjryan force-pushed the brendanjryan/validate-settle-credentials branch from e4a6b90 to 1a46b24 Compare June 30, 2026 01:08

@tempoxyz-bot tempoxyz-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

👁️ Cyclops Review

PR #597 preserves the existing settlement path, but the new advisory Tempo validation surface has two actionable security gaps. Details are inline.

Reviewer Callouts
  • Advisory API semantics: Downstream integrations may use validateCredential for pre-auth/risk workflows; document exactly what it guarantees and do not present it as proof of payment until replay and sender-authentication gaps are closed.
  • Future split-method hooks: No current method defines both validate and method-level settle, but future methods that do must keep settle() independently sound and not rely on validate() side effects.
  • Validation result/error contract: Validation.source echoes credential.source, and validateCredential() can surface raw RPC/viem errors; consumers should use method-authenticated fields and handle non-PaymentError failures.

throw new MismatchError('Only Tempo (0x76/0x78) transactions are supported.', {})

const client = await getClient({ chainId })
const transaction = Transaction.deserialize(serializedTransaction)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 [SECURITY] Tempo pull validation trusts unauthenticated deserialized senders

This branch treats Transaction.deserialize(serializedTransaction).from plus signature presence as authenticated identity and later returns it as details.sender. Tempo envelopes can carry an explicit sender slot in both 0x78 and 0x76 forms; the parser populates from from that slot and only recovers from the appended signature if from is absent. A challenge-bound transaction can therefore validate while naming a victim sender even though the raw sender signature was produced by someone else.

Recommended Fix: Recover/verify the sender signature against the exact submitted envelope before trusting transaction.from; reject explicit sender slots that are not cryptographically bound, and do not treat parser output or eth_call as proof of signature ownership.

return request as unknown as z.output<typeof Methods.charge.schema.request>
})()
const chainId = resolvedRequest.methodDetails?.chainId ?? request.chainId
async validate({ credential, request }) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 [SECURITY] Tempo validation reports consumed or un-settleable credentials as valid

The new non-mutating validation hook never reads the replay markers that settlement writes for hash, proof, and pull transaction credentials. A credential that has already been settled can still return a successful validateCredential() result even though settleCredential() would reject it as used. The sponsored pull branch also only performs static call/fee-token checks and skips settlement's sponsored-sender reservation, sponsor policy/filling, and pre-broadcast simulation.

Recommended Fix: Keep validation non-mutating but read replay keys with Store.get before success, and share read-only/pure sponsored-transaction policy checks plus non-mutating simulation where possible. If a full settleability check cannot be done, return an explicit lower-confidence/shape-only validation result instead of unconditional success.

@brendanjryan brendanjryan force-pushed the brendanjryan/validate-settle-credentials branch from 25a5330 to e15a622 Compare June 30, 2026 17:21
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.

2 participants