Skip to content

feat(smoke): x402 gate proves a paywalled route answers 402, rejects forged payments and, with CHAIN, accepts a real one - #67

Open
ddpateltp wants to merge 9 commits into
hedera-dev:devfrom
ddpateltp:feat/x402-smoke-gate
Open

ddpateltp wants to merge 9 commits into
hedera-dev:devfrom
ddpateltp:feat/x402-smoke-gate

Conversation

@ddpateltp

@ddpateltp ddpateltp commented Sep 12, 2026

Copy link
Copy Markdown

SMOKE x402 gate: prove a route is really paywalled

Problem

Ask a coding agent for "an x402-gated endpoint on Hedera" and you routinely get one of these back:

  • a route that answers 200 to everyone (the paywall is not enforced),
  • a 402 with an x402 v1 JSON body and no PAYMENT-REQUIRED header (no v2 client can pay it),
  • a PAYMENT-REQUIRED whose accepts[] prices HBAR in decimals, names eip155:296 instead of hedera:testnet, or has no extra.feePayer (the Hedera exact scheme MUST),
  • a server that returns 200 for any PAYMENT-SIGNATURE header, or crashes on a malformed one.

The Playwright gate cannot see any of that: a JSON route that returns 200 is a passing route, and the semantic validator only gets to look after SMOKE is green. Today the harness's only x402 knowledge is prose in the validator prompt, so a broken paywall is graded by an LLM reading a transcript, if at all.

What this adds

validators.x402 points at a small YAML on the SMOKE dev server. The gate speaks the protocol deterministically, in seconds, and every mistake becomes a finding the runtime repair prompt can act on:

Probe Must hold Finding
unpaid request HTTP 402 + PAYMENT-REQUIRED header x402:route:<name>:status / :header
header decodes base64 JSON, x402Version: 2, resource.url :decode / :version / :resource
every accepts[] entry exact, the recipe's CAIP-2 network, whole-unit amount, asset 0.0.0 or HTS id, payTo, extra.feePayer; recipe ceilings maxAmount / payTo / asset :requirement listing each violated rule
forged PAYMENT-SIGNATURE rejected with 402, never 2xx or 5xx :tamper
facilitatorUrl (opt-in) GET /supported lists exact on that network x402:facilitator:supported
pay: true (opt-in, needs CHAIN) the harness pays with the ephemeral signer, gets 2xx + PAYMENT-RESPONSE { success, transaction }, and the mirror node shows payTo credited exactly amount :pay / :settlement

The paid probe builds the partially signed TransferTransaction the Hedera exact scheme specifies (payer to payTo, transactionId.accountId = extra.feePayer, payer signature only) using the @hiero-ledger/sdk the harness already ships and the CHAIN signer it already funds. A green gate is therefore the "real paid request end to end", produced by the harness rather than read off an agent transcript. Paying on hedera:mainnet is refused at config load.

doctor validates the gate config (and that pay: true has a signer) before a run; validate runs the unpaid probes; x402 findings repair in the runtime scope alongside Playwright failures, and prompts/repair-runtime.md tells the agent what a correct 402 looks like.

How to run it

# .harness/spec.yaml
validators:
  playwright: .harness/validators/playwright-smoke.yaml
  x402: .harness/validators/x402.yaml
# .harness/validators/x402.yaml (see skeletons/project-harness/validators/x402.example.yaml)
routes:
  - name: quote
    path: /api/quote
    maxAmount: "1000000"      # tinybars
hedera-harness doctor          # x402 gate: 1 route(s) on hedera:testnet
hedera-harness validate        # unpaid probes on the SMOKE server
hedera-harness run             # gate runs inside SMOKE; pay: true pays with the CHAIN signer
node --test test/x402-gate.test.mjs   # 16 tests against a local fixture resource server

All 16 tests run fully offline. The test starts its own fixture resource server on a loopback port, needs no network access and no testnet credentials, and the signer is an ECDSA key generated inside the test. The pay: true path is exercised end to end against that fixture, which deserialises the Hedera transaction and checks the transfer the way a facilitator would, with the mirror-node lookup answered by a stubbed fetchImpl (a 404 first, then the settled transaction) so the settlement check is covered without a round trip to testnet.

Before / after

Before: a generated /api/quote that returns 200 {"quote":…} to an unpaid request passes SMOKE. After: x402 route /api/quote served the resource without payment (HTTP 200) — the paywall is not enforced, SMOKE fails in under a second, and the repair prompt names the exact rule. The test fixture covers seven distinct broken paywalls, each producing exactly one finding, plus a correct one that passes and a paid flow the fixture verifies the way a facilitator would.

Related PRs

#44 adds a doctor check that the x402 facilitator answers /supported, and #54 adds an x402Settlement flag that asks the validator prompt to look for settlement. Both improve preflight and prose. This PR is different in kind: it is a deterministic SMOKE gate that speaks the protocol against the dev server and fails with a named rule, so a broken paywall never reaches the LLM validator. The optional facilitatorUrl cross-check overlaps with #44 in intent; if #44 lands first the gate can defer to it.

Scope notes

  • Additive: recipes without validators.x402 are unchanged. ValidationFinding.category gains "x402".
  • Requires validators.playwright (the dev server lives there); the loader says so.
  • Protocol references: x402 specs/transports-v2/http.md and specs/schemes/exact/scheme_exact_hedera.md.

Built for ETHOnline 2026 (Hedera "Improve the Hedera Harness" track) while building an x402-gated agent marketplace on Hedera, where exactly these failures showed up in generated code.

An unpaid request must answer 402 with a base64 PAYMENT-REQUIRED header; every accepts[] entry is graded against the Hedera exact scheme (scheme, CAIP-2 network, whole-unit amount, asset, payTo, extra.feePayer) and the recipe ceilings; a forged PAYMENT-SIGNATURE must be rejected. With pay: true and a CHAIN signer the gate builds the partially signed TransferTransaction the scheme specifies, pays once, and requires a successful PAYMENT-RESPONSE whose transaction credits payTo on the mirror node. Paying on mainnet is refused.
…in the runtime scope

validators.x402 borrows the Playwright dev server, so the loader requires validators.playwright alongside it. Both run and validate execute the gate; x402 findings join commands and playwright in the runtime repair scope.
A typo in x402.yaml, or pay: true without chainValidation, used to surface only after GENERATE and a dev-server boot.
The fixture verifies the harness's payment the way a facilitator would:
it deserialises the Hedera transaction and checks the transfer, so the
paid probe is exercised end to end without testnet.
@ddpateltp ddpateltp changed the title feat(smoke): x402 gate — prove a paywalled route answers 402, rejects forged payments, and (with CHAIN) accepts a real one feat(smoke): x402 gate proves a paywalled route answers 402, rejects forged payments and, with CHAIN, accepts a real one Sep 12, 2026
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.

1 participant