Skip to content

feat(cli): add mppx validate command for end-to-end server validation#595

Merged
brendanjryan merged 3 commits into
wevm:mainfrom
bensandler-stripe:bensandler/cli-validate
Jul 1, 2026
Merged

feat(cli): add mppx validate command for end-to-end server validation#595
brendanjryan merged 3 commits into
wevm:mainfrom
bensandler-stripe:bensandler/cli-validate

Conversation

@bensandler-stripe

Copy link
Copy Markdown
Contributor

Overview

Validates MPP server implementations by testing discovery, challenges, error handling, and end-to-end payment flows.

Right now, this only does end-to-end livenet/testnet payments with tempo, but I plan to add support for stripe payments in a follow-up.

When a server defines a openapi.json, the validate command automatically discovers and hits machine-payable endpoints, validating their behavior and error handling. The tool constructs request bodies for these endpoints in the following priority order (1) one provided explicitly with --body, (2) an example body documented in the openapi.json, (3) an auto-generated body using the API schema.

CLI subcommand structure:

  src/cli/validate/
    index.ts      - command definition and orchestration
    challenge.ts  - challenge parsing and error handling checks
    payment.ts    - payment flow, wallet provisioning, receipt validation
    discovery.ts  - OpenAPI fetching, endpoint extraction, body generation
    helpers.ts    - shared types, utilities, output formatting

Demonstration

livenet server with openapi.json:

$ mppx validate https://parallelmpp.dev

Discovery (/openapi.json)
  ✓ Document found and parseable
  ✗ Valid OpenAPI structure (2 error(s))
    paths./api/task/{runId}.get.responses: Operation with x-payment-info MUST have a 402 response
    paths./api/wallet/balance/{address}.get.responses: Operation with x-payment-info MUST have a 402 response
  ✓ Paid endpoints found (5 endpoint(s))

POST /api/search
  Challenge
  ✓ Returns 402 without credentials
  ✓ WWW-Authenticate header present (Payment scheme)
  ✓ Challenge parseable (tempo/charge)
  ✓ Challenge has id
  ✓ Challenge has realm
  ✓ Challenge expires in the future (5m from now)
  ✓ Realm matches server hostname
  ✓ Valid recipient address
  ✓ Valid currency address (mainnet)
  ✓ Amount is valid integer string
  Error Handling
  ✗ Malformed credential returns 402 (Got 500 (server error))
    → When the Authorization header contains an invalid credential, respond with 402 (not 500). Catch credential validation errors and return a fresh challenge.
  Payment
    Using wallet: 0x37138facb18b3Da341004646D29E3c75F7a3beAF

▸   Mainnet payment: Will transfer $0.01 to 0x4021a678... Continue? (y/N) y
  ✓ Payment: credential created
  ⚠ Payment: post-payment response (Got 400)
    → Payment succeeded but the endpoint returned a client error. The endpoint likely requires request body parameters. Use --body to provide them.
  ✓ Payment-Receipt header present
  ✓ Receipt parseable
  ✓ Receipt status is "success"
  ✓ Receipt reference valid (0xd613f9f045e76527a2...)
  ✓ Receipt timestamp recent (0s ago)
  ✓ Response body non-empty (application/json, 164B)
  ✓ Content-Type header set (application/json)
  ✓ On-chain transaction (https://explore.tempo.xyz/receipt/0xd613f9f045e76527a26659aa5d9910f846d4e98f660fadf33be5b0c3a086889b)

Without an openapi.json, the CLI prints

$ mppx validate https://mpp.browserbase.com

Discovery (/openapi.json)
  ✗ Document found (HTTP 404)

  No discovery document found.
  MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.
  In the meantime, to test a specific endpoint, you can use mppx validate <url> --endpoint POST:/your/path

The user can then pass a --endpoint override to test

$ mppx validate https://mpp.browserbase.com --endpoint POST:browser/session/create

Discovery (/openapi.json)
  ✗ Document found (HTTP 404)
    → MPP servers must serve an OpenAPI document at /openapi.json with x-payment-info extensions.

POST browser/session/create
  Challenge
  ✓ Returns 402 without credentials
  ✓ WWW-Authenticate header present (Payment scheme)
  ✓ Challenge parseable (tempo/charge)
  ✓ Challenge has id
  ✓ Challenge has realm
  ✓ Challenge expires in the future (5m from now)
  ⚠ Realm matches server hostname (realm="ip-172-31-3-82.ec2.internal" vs host="mpp.browserbase.com")
    → Set the realm to your production hostname (or base domain) in the challenge. Clients use realm to verify they are paying the right server.
  ✓ Valid recipient address
  ✓ Valid currency address (mainnet)
  ✓ Amount is valid integer string
  Error Handling
  ✓ Malformed credential returns 402 (not 500)
  ✓ Error response includes fresh challenge
  Payment
    Using wallet: 0x37138facb18b3Da341004646D29E3c75F7a3beAF

▸   Mainnet payment: Will transfer $0.01 to 0x3e4bb7be... Continue? (y/N) y
  ✓ Payment: credential created
  ✓ Payment: successful (HTTP 200)
  ✓ Payment-Receipt header present
  ✓ Receipt parseable
  ✓ Receipt status is "success"
  ✓ Receipt reference valid (0x4702cfa7c4104ef0e0...)
  ✓ Receipt timestamp recent (1s ago)
  ✓ Response body non-empty (application/json, 1.0KB)
  ✓ Content-Type header set (application/json)
  ✓ On-chain transaction (https://explore.tempo.xyz/receipt/0x4702cfa7c4104ef0e09575b0a790fa3aec09f2cce197e9fbe2d6f1ad3333a473)

Summary: 21 passed, 1 failed, 1 warning(s)

  Tip: validate a testnet server too for free. This CLI automatically provisions and funds a testnet wallet for testing.

If a server supporting the testnet is provided, the CLI will automatically provision and fund a testnet wallet for testing.

$ mppx validate https://starlink-payment-test.vercel.app


Discovery (/openapi.json)
  ✓ Document found and parseable
  ✓ Valid OpenAPI structure
  ✓ Paid endpoints found (1 endpoint(s))

POST /api/flight-starlink
  Challenge
  ✓ Returns 402 without credentials
  ✓ WWW-Authenticate header present (Payment scheme)
  ✓ Challenge parseable (tempo/charge)
  ✓ Challenge has id
  ✓ Challenge has realm
  ✓ Challenge expires in the future (5m from now)
  ✓ Realm matches server hostname
  ✓ Valid recipient address
  ✓ Valid currency address (testnet)
  ✓ Amount is valid integer string
  Error Handling
  ✓ Malformed credential returns 402 (not 500)
  ✓ Error response includes fresh challenge
  Payment
    Provisioning testnet wallet and funding via faucet...
    Using wallet: 0xacEEF24d1138992822AFb50b78642E8E024871B0
  ✓ Payment: credential created (ephemeral testnet wallet)
  ✓ Payment: successful (HTTP 200)
  ✓ Payment-Receipt header present
  ✓ Receipt parseable
  ✓ Receipt status is "success"
  ✓ Receipt reference valid (0x616d4a3d4485bb753a...)
  ✓ Receipt timestamp recent (1s ago)
  ✓ Response body non-empty (application/json, 373B)
  ✓ Content-Type header set (application/json)
  ✓ On-chain transaction (https://explore.testnet.tempo.xyz/receipt/0x616d4a3d4485bb753a2d7eeee176868f706f3f8144426a1e8c368e23e0c22c08)

Summary: 25 passed

  Tip: validate your mainnet server too to confirm real payments work end-to-end.

  Validates MPP server implementations by testing the full protocol flow:
  discovery, challenge structure, error handling, and payment round-trip.

  Structure:
    src/cli/validate/
      index.ts      - command definition and orchestration
      challenge.ts  - challenge parsing and error handling checks
      payment.ts    - payment flow, wallet provisioning, receipt validation
      discovery.ts  - OpenAPI fetching, endpoint extraction, body generation
      helpers.ts    - shared types, utilities, output formatting

  Features:
  - Zero-setup testnet validation (ephemeral wallet + faucet)
  - Pre-flight balance check on mainnet with correct wallet resolution
  - --yes/-y for non-interactive payment approval
  - --endpoint for manual single-endpoint testing (skips discovery)
  - --body with per-path mapping support in discovery mode
  - Bare-request-first strategy (retry with body on 400)
  - Recursive request body generation from OpenAPI schemas
  - Correctly identifies x402 endpoints as non-MPP
  - Tempo explorer links for on-chain proof
  - 25 end-to-end integration tests

  Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

  GIT_VALID_PII_OVERRIDE

@brendanjryan brendanjryan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I like this! Some notes on implementation complexity / structure

Comment thread src/cli/validate/helpers.ts Outdated

export const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'])

export const MAINNET_CHAIN_ID = 4217

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

assuming this will only work for tempo? I thin we can pull this and some of other others from shared constants

Comment thread src/cli/validate/index.ts
const validate = Cli.create('validate', {
description: 'Validate an MPP server implementation end-to-end',
args: z.object({
url: z.string().describe('Base URL of the MPP server to validate'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

can this work on local services too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yep this will work on local services!

Comment thread src/cli/validate/index.ts
.default(0)
.meta({ count: true })
.describe('Verbosity level'),
yes: z.boolean().default(false).describe('Auto-approve mainnet payments'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

testnet as well? or are testnet payments automatically approved always?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

they're automatically approved (does that sound reasonable to you?)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

yeah I think that's fine on testnet

Comment thread src/cli/validate/index.ts Outdated
process.exit(1)
}

// Summary

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this method is a little gnarly / long .. can we separate each stage into its own functional check?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

agreed, let me take a stab at that!

Comment thread src/cli/validate/payment.ts Outdated
const account = privateKeyToAccount(key)

const { createClient, http } = await import('viem')
const { tempoModerato } = await import('viem/tempo/chains')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

why do we need so many async imports here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ah, it's so make the CLI a bit faster (saves about ~250ms upfront). this is probably a premature optimization, I'll undo this.

@@ -0,0 +1,391 @@
import * as Challenge from '../../Challenge.js'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

from a skim this file seems correct but also duplicates a ton of functinoality and relies on a lot of manual parsing -- is this strictly necessary? can we pull from more things in mppx

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👀

@bensandler-stripe bensandler-stripe Jun 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I was able to re-use a bit more, but the core reason why this duplicates so much is that mppx.fetch is a monolith that does all of the following at once:

  1. Makes initial request
  2. Checks if 402
  3. Selects challenge + method
  4. Creates credential
  5. Retries with credential
  6. Emits events

We want to run each piece at a time here to validate. We could consider decomposing that monolith fetch into the parts we need here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ah I see -- I think this is fine to have a little verbose for now. we can always collapse later

@bensandler-stripe bensandler-stripe force-pushed the bensandler/cli-validate branch 5 times, most recently from 6678f63 to 45aa1d3 Compare June 29, 2026 20:21
@bensandler-stripe bensandler-stripe force-pushed the bensandler/cli-validate branch from 45aa1d3 to 370b133 Compare June 29, 2026 20:40
@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@595

commit: dc0c41f

@brendanjryan brendanjryan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

- Use shared tempoChainIds from internal/defaults instead of hardcoded constant
- Extract printResults/Counts to deduplicate result counting
- Switch to static imports in payment.ts (measured ~230ms, negligible)
- Auto-skip mainnet prompt in non-interactive mode (no TTY)
- Extract resolveWalletAddress to shared account.ts helper
- Break run() into discoverEndpoints/validateEndpoints/printSummary phases

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Committed-By-Agent: claude
@bensandler-stripe bensandler-stripe force-pushed the bensandler/cli-validate branch from 370b133 to 585ddcc Compare July 1, 2026 19:31
  Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

  GIT_VALID_PII_OVERRIDE
@brendanjryan brendanjryan merged commit 5f62a4b into wevm:main Jul 1, 2026
13 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.

2 participants