Skip to content

fix(x402): validate business params before issuing the payment challenge - #18

Merged
edycutjong merged 2 commits into
mainfrom
fix/validate-before-payment
Aug 20, 2026
Merged

edycutjong merged 2 commits into
mainfrom
fix/validate-before-payment

Conversation

@edycutjong

Copy link
Copy Markdown
Owner

Fixes the OKX.AI delisting of agent #6150 (2026-08-20): both services CLV Grade and CLV Audit "only prompt for missing or incorrect parameters after the buyer completes the payment signature and deduction."

Root cause

okxPayGate (the SDK's paymentMiddleware) knows nothing about our business params. It issued the challenge, verified the PAYMENT-SIGNATURE, and settled β€” and only then did the handlers parse the body, answering an invalid call with HTTP 200 and a usage note. The buyer paid for an error message.

Fix β€” api/validate.ts::paramPreflight, mounted ahead of okxPayGate

Case Behaviour
unpaid, param-less GET passes through β†’ 402 challenge (marketplace reachability probe / onchainos payment quote)
invalid params, unpaid 400, challenge never issued
invalid params, payment attached 400 before verify/settle β€” the authorization nonce is never spent
valid params next(), payment proceeds unchanged

Schemas moved out of api/routes.ts into api/validate.ts, so a request can never clear preflight and then fail inside a handler that has already been paid for. bets sent as a JSON string on a GET is now parsed, so paid GET /api/audit matches POST.

Verification

  • 155 tests green (was 130), lint + typecheck clean, 99.26% statements.
  • test/prepay-validation.test.ts (22 cases) pins all three cases plus the deduction guarantee: after a rejected paid call the same signature still buys a real grade, proving nothing was settled.
  • Passes OKX's own validator on all three paths:
    onchainos agent x402-check --endpoint … [--body …] β†’ valid: true.

Behaviour changes

Two tests asserted the rejected contract by name and are inverted β€” "a malformed body still gets 402 before validation" and "returns a 200 usage response for a paid-but-malformed body". Suites that probed the challenge with an empty {} body now probe with valid params.

OKX.AI delisted CLV Scout because both services "only prompt for missing
or incorrect parameters after the buyer completes the payment signature
and deduction" β€” review requires validation to complete before the x402
challenge is returned.

Root cause: okxPayGate (the SDK's paymentMiddleware) knows nothing about
our business params, so it issued a challenge, then verified and settled
the replay, and only then did gradeHandler/auditHandler parse the body β€”
answering an invalid call with HTTP 200 and a usage note. The buyer paid
for an error message.

Fix: api/validate.ts::paramPreflight, mounted ahead of okxPayGate.

  1. unpaid + param-less GET  -> pass through to the gate (the marketplace
     reachability probe / `onchainos payment quote` must keep receiving a
     well-formed PAYMENT-REQUIRED challenge, or the listing reads as
     endpoint_unreachable β€” see d4bb5fa).
  2. invalid params, paid or not -> 400 here. Unpaid, no challenge is ever
     issued. Paid, the PAYMENT-SIGNATURE is never verified and never
     settled, so a buyer holding a challenge from case 1 is NOT charged.
  3. valid params -> next(), payment proceeds exactly as before.

The preflight parses with the SAME schemas api/routes.ts uses (moved into
api/validate.ts), so a request can never clear preflight and then fail in
a handler that has already been paid for. Handlers now answer a bad body
with 400 + the copyable example instead of 200; that path is defence in
depth, unreachable through the mounted stack.

Also: `bets` sent as a JSON string on a GET is now parsed, so paid GET
/api/audit works the same as POST.

Tests: test/prepay-validation.test.ts (22 cases) pins all three cases plus
the deduction guarantee β€” after a rejected paid call the authorization
nonce is still unspent, so the very same signature still buys a real
grade. Suites that probed the challenge with an empty `{}` body now probe
with valid params, and the two tests that asserted the old behaviour
("402 before validation", "200 usage response for a paid-but-malformed
body") are inverted. 155 tests green.
Stage 4 probed the paid routes with `-d '{}'` and asserted 402 β€” the old
contract. Under the new preflight a param-less POST is answered 400 by
design, so the smoke stage failed on its own success criterion.

- grade/audit challenge probes now send VALID params to earn the 402.
- Two new steps assert the listing rule itself: an invalid call returns
  400 and carries NO PAYMENT-REQUIRED response header. The check greps
  the header line (`^payment-required:`) rather than the string, because
  the CORS Access-Control-Expose-Headers line also names that header.
- The bare-GET probe assertion is unchanged: discovery still gets 402 +
  PAYMENT-REQUIRED, which is what the marketplace validator reads.

Verified locally against the real entrypoint: 402/402/400/400/402/200.
@edycutjong
edycutjong merged commit 418c7f1 into main Aug 20, 2026
8 checks passed
@edycutjong
edycutjong deleted the fix/validate-before-payment branch August 20, 2026 13:38
@edycutjong

Copy link
Copy Markdown
Owner Author

πŸŽ‰ This PR is included in version 1.0.7 πŸŽ‰

The release is available on GitHub release

Your semantic-release bot πŸ“¦πŸš€

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant