Skip to content

feat(x402): seller-side server executor + Express/FastMCP adapters - #119

Open
pcarranzav wants to merge 5 commits into
mainfrom
pcv/trm-ts-server-executor
Open

feat(x402): seller-side server executor + Express/FastMCP adapters#119
pcarranzav wants to merge 5 commits into
mainfrom
pcv/trm-ts-server-executor

Conversation

@pcarranzav

Copy link
Copy Markdown
Member

Seller-side x402 server executor (TypeScript)

Brings the seller-side compliance gate to the TypeScript SDK, mirroring the Python AmpersendX402ServerExecutor / require_payment. Net-new src/x402/server/ module — no existing behavior changes.

What's added

  • X402ServerExecutor interface (verifyPayment / settlePayment), using @x402/core VerifyResponse/SettleResponse.
  • FacilitatorX402ServerExecutor — delegates verify+settle to an x402ResourceServer over HTTPFacilitatorClient.
  • AmpersendX402ServerExecutor — composes the facilitator executor and gates incoming payments on Ampersend's POST /api/v1/agents/:address/payment/authorize-receipt:
    • SIWE-bearer auth under the seller agent's identity (reuses ApiClient), with refresh-on-401 (re-auth once, retry once).
    • 5s timeout via AbortController, override AMPERSEND_COMPLIANCE_API_TIMEOUT_SECONDS (seconds) — matches the Python env var.
    • Fail closed on a compliance deny and on any transport/timeout/HTTP error: the buyer gets a generic "Payment rejected" and the payment is not honored. No reason/reasonCode/screeningId leaked to the buyer; full detail logged at WARNING server-side.
    • Non-exact scheme → deny "Unsupported payment scheme".
    • This deliberately diverges from the Python A2A executor (which propagates outages → 500): the TS v1 surfaces are HTTP/MCP, where a 500 invites blind retry under outage — same choice the Python FastAPI/HTTP middleware makes. Documented in a code comment.
  • Adapters: withAmpersendX402Payment (framework-agnostic core), an Express middleware (ampersendPaymentMiddleware, behind the ./x402/server/express subpath), and a FastMCP wiring (createExecutorOnPayment / withAmpersendX402PaymentMcp) backing the existing withX402Payment onPayment. All fail closed.

Auth/client

  • ApiClient.authorizeReceipt(params, { signal }) added (discriminated-union response, refresh-on-401), and the internal fetch now honors a caller AbortSignal combined with the default-timeout signal.

Exports

  • @ampersend_ai/ampersend-sdk / /x402 / /x402/server: the executors, withAmpersendX402Payment, FastMCP helpers, GENERIC_DENY_REASON.
  • @ampersend_ai/ampersend-sdk/x402/server/express: ampersendPaymentMiddleware (express is type-only here + behind its own subpath, so non-Express consumers pull no new coupling).

Note on express (one rejected review finding)

The spec said express should be an optional peer dep, but express is already a hard runtime dependency of the shipped MCP proxy (src/mcp/proxy/server/server.ts, the ampersend-proxy bin) on main — demoting it would break that. The intent (don't force express on consumers via the new adapter) is satisfied instead by a type-only import + dedicated subpath (dist/x402/server/express.js emits zero express require). No version bump; @x402/express not added (the adapter reimplements the route-keyed shape to route through the executor).

Testing

pnpm build / pnpm lint / pnpm format clean; 281/281 tests (28 files), incl. 18 new: deny→generic/no-leak, allow→verify→settle, facilitator failure, network/timeout fail-closed, 401→reauth+retry, unsupported-scheme, Express deny→403, FastMCP deny→402.

Consumed by the Ampersend monorepo via submodule pointer bump (which swaps x402-demo-api onto this adapter). Based off SDK main.

pcarranzav and others added 4 commits July 1, 2026 19:41
Introduce X402ServerExecutor, a FacilitatorX402ServerExecutor, and an
AmpersendX402ServerExecutor that gates incoming payments on the Ampersend
authorize-receipt compliance endpoint (SIWE-bearer auth, refresh-on-401,
5s timeout). Adds Express and FastMCP adapters. Both surfaces fail closed:
a compliance deny or an Ampersend outage yields a generic "Payment rejected"
and the payment is not honored. No reason/code/screening-id is leaked to the
buyer; full detail is logged server-side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Re-exporting the server executors from src/x402/index.ts pulled the
node-only server deps (@x402/core/server -> node:module) into every
consumer of that barrel — including ampersend/treasurer.ts, which is
bundled into downstream browser/client builds. The executors remain
available via the dedicated @ampersend_ai/ampersend-sdk/x402/server
and .../x402/server/express subpaths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Express adapter advertised x402Version: 1 but forwarded the @x402/core
v2 requirements shape (amount/asset/extra), so x402 v1 buyers (and the
authorize-receipt v1-wire contract) couldn't parse it. Introduce an
AmpersendPaymentRequirements superset (v1 maxAmountRequired/resource/
description/mimeType + v2 amount) and fill resource per-request from the
requested URL, so v1 buyers, authorize-receipt, and the @x402/core
facilitator are all satisfied.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebasing onto main (which migrated to Effect v4, #105) changed
Schema.Union to take an array of members rather than variadic args.
Wrap the two discriminated-union branches in an array to match the
v4 idiom used elsewhere in the codebase.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M9mghCaNun4xKteXGWxPGP
@pcarranzav
pcarranzav force-pushed the pcv/trm-ts-server-executor branch from 8288d69 to 5771daf Compare July 1, 2026 19:43
Reviewer nit: clarify that the FastMCP deny path emits 402 (retry-invited)
while the Express adapter uses 403 to short-circuit retries. Both remain
fail-closed and leak-free; comment-only, no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M9mghCaNun4xKteXGWxPGP
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