Skip to content

sdk: accept payments from any HTTP stack via radius-sdk/server - #44

Open
azf20 wants to merge 1 commit into
mainfrom
sdk/framework-agnostic-server
Open

azf20 wants to merge 1 commit into
mainfrom
sdk/framework-agnostic-server

Conversation

@azf20

@azf20 azf20 commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Makes the seller side framework-agnostic. The Radius-specific pieces (facilitator client, exact scheme, SBC pricing, gas-sponsoring rules) now live in one x402 resource server that does not know how requests arrive; adapters are thin layers over it.

What changes

New radius-sdk/server entry point:

  • radiusPayments() — web-standard handler, Request in, Response out. Runs on Cloudflare Workers without a framework, Bun, Deno, Node 18+, and Next.js / SvelteKit / Remix route handlers. Paid handlers receive the settled receipt as a second argument; wrap() returns a single fetch function; requiresPayment() checks a route without I/O.
  • createRadiusServer() — exposes the x402 resource server (radius.server) and a routes() builder, so the upstream adapters work directly: paymentMiddleware(radius.routes({ … }), radius.server) with @x402/express, @x402/next or @x402/hono. radius.http() returns an x402HTTPResourceServer for their …FromHTTPServer variants.
  • onSettled is registered through x402 core's after-settle hook, so it fires whichever adapter served the request. It receives the x402 request context; requestOf(context) returns the Request for the SDK's own adapters. Behaviour change: an error thrown inside onSettled is logged by core rather than failing the request.

radius-sdk/hono is now a thin wrapper over the handler: same options, dynamic payTo/price/onSettled still receive the Hono context, c.get('radiusPayment') unchanged. RadiusHonoAdapter is removed. facilitator.ts and scheme.ts move from src/hono/ to src/server/ (still re-exported from radius-sdk/hono).

@x402/core / @x402/evm go from 2.25.0 to 2.27.0, the line the upstream adapters depend on. The SDK's own handler keeps lazy, request-time facilitator initialisation; the upstream adapters fetch /supported at construction by default and their syncFacilitatorOnStart: false means never rather than lazily, so on Workers callers must await radius.server.initialize() themselves. README says so.

Examples: examples/worker-plain (no framework, port 8788) and examples/express-seller (@x402/express, port 8789), same API as the Hono worker-seller.

Verified

  • 123 unit tests + import guard. New test/server.test.ts covers the handler (challenge shape, dynamic payTo/price, browser paywall, settle before/after, failed settlement, malformed header, immutable upstream headers, handler failure paths) and runs Express through the real @x402/express adapter with a mocked facilitator.
  • 17 e2e tests on testnet, including a real settlement through the plain handler with onSettled observed.
  • Stock radius-cli wallet x402 paid both new examples on testnet: worker-plain under wrangler dev (0xb9e874a455fec5ad1bb39b35ff9614b56a1a2e93fbd859b77cb7f8bc2c62289a), express-seller on Node (0xcd44add4aa1f43c5a7613efa23b27ad52efae7843e28a5188d508341cc80615e). Both logged the receipt through onSettled.
  • changeset status: radius-sdk minor, radius-cli patch.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PtERNnagj1qYiSHvfLpdtu

@azf20
azf20 force-pushed the sdk/framework-agnostic-server branch from f041465 to 45b784c Compare October 5, 2026 12:22
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: 2a540b6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
radius-sdk Minor
radius-cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

New `radius-sdk/server` entry point holding the seller core independently of
how requests arrive:

- `radiusPayments()` is a web-standard handler (Request in, Response out) for
  Workers without a framework, Bun, Deno, Node 18+, and Next.js / SvelteKit /
  Remix route handlers. Paid handlers receive the settled receipt; `wrap()`
  gives a single fetch function; `requiresPayment()` is a no-I/O route check.
- `createRadiusServer()` exposes the Radius x402 resource server and a
  `routes()` builder for the upstream adapters:
  `paymentMiddleware(radius.routes({...}), radius.server)` with
  `@x402/express`, `@x402/next` or `@x402/hono`.
- `onSettled` is registered via x402 core's after-settle hook so it fires for
  every adapter. Errors it throws are logged by core instead of failing the
  request.
- `radius-sdk/hono` becomes a thin wrapper over the handler with unchanged
  options and tests; `RadiusHonoAdapter` is removed. facilitator.ts and
  scheme.ts move to src/server/.
- `@x402/core` and `@x402/evm` 2.25.0 -> 2.27.0 (the line the upstream
  adapters require).
- Examples: `examples/worker-plain` (no framework) and
  `examples/express-seller` (`@x402/express`), same API as the Hono worker.

Verified: 123 unit tests + import guard (new test/server.test.ts covers the
handler and Express through the real upstream adapter); 17 e2e tests on
testnet including a real settlement through the plain handler; stock
`radius-cli wallet x402` paid worker-plain under `wrangler dev`
(0xb9e874a455fec5ad1bb39b35ff9614b56a1a2e93fbd859b77cb7f8bc2c62289a) and
express-seller on Node
(0xcd44add4aa1f43c5a7613efa23b27ad52efae7843e28a5188d508341cc80615e).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PtERNnagj1qYiSHvfLpdtu
@azf20
azf20 force-pushed the sdk/framework-agnostic-server branch from 45b784c to 2a540b6 Compare October 5, 2026 22:00
@azf20
azf20 requested review from erikzrekz and kylecrawshaw and removed request for kylecrawshaw October 6, 2026 17:50

erikzrekz commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

From Codex:

I tested an independent seller while evaluating this generic server shape: @emdash-cms/x402 1.1.0 on an EmDash Astro/Cloudflare dev server, paid by radius-cli 0.3.0 on Radius testnet. The synthetic route returned a protected JSON body and PAYMENT-RESPONSE; tx 0x2afd718c3f24bbae9468ee1ec7812334ea5292757402ac84ee9b3f5bda141add has a successful receipt and a 0.01 SBC transfer. A CMS-backed route also settled (tx 0x0215c817df47c9919fcb85610cc09051273f9abd7765eedb51c608b03c90e6c4), but returned {"pages":[]} because that local CMS database was unseeded. This validates the shared x402 wire path, while illustrating a seller delivery risk.

Two additions would help radius-sdk/server users:

  1. Show an Astro APIRoute example that loads and validates the entry before calling pay(request, handler). With the default settle: 'before', a missing entry or handler failure after settlement can charge a buyer without delivering useful content. The example can return 404 before payment, then pass the prepared content to the paid handler.
  2. State explicitly that onSettled records settlement, not successful delivery. It can fire before the application handler, and this PR notes that hook errors are logged rather than failing the request. For audit/recovery, keep the transaction receipt and delivery outcome separately; after a paid 5xx or uncertain 502, reconcile the transaction before asking the buyer to sign again.

The new malformed-header and failed-settlement tests already cover two faults the EmDash package exposed (its malformed header produced 500; its paid flag can be true when settlement failed). Those are useful guarantees to retain across adapters.

@claude

claude Bot commented Oct 6, 2026

Copy link
Copy Markdown

Review of the new radius-sdk/server core at 2a540b6. The design looks right: one RadiusServer core with thin web-standard, Hono, and Express/Next layers on top. CI is green, and locally pnpm -r typecheck is clean and vitest passes 152/152. A few items before merge:

1. Settle-before mode buffers every paid response, which breaks streaming. await res.arrayBuffer() runs on every paid response, but in the before flow x402 core's processSettlement never reads the body. SSE and LLM token streams will stall or arrive all at once (same through the Hono wrapper). Suggested fix: when settlement already happened before the handler, return the original response stream with createSettlementHeaders(...) attached and buffer only in after mode. A test whose handler returns a ReadableStream would lock this in. (Found by reading the code; I haven't pushed a live stream through it.)

2. A buyer can pay and get nothing (comment item 1). With the default settle-before, a handler 4xx/5xx after settlement returns the error with a receipt after funds have moved. Suggest a pre-payment check hook (e.g. before(request) => Response | void) or a documented "validate, then pay" pattern, plus the Astro APIRoute example that returns 404 before charging.

3. onSettled semantics (comment item 2). In before mode onSettled fires before the handler runs, and errors it throws are only logged. Worth stating in the doc comment and README that it records settlement, not delivery, with a short note on reconciling after a paid 5xx.

4. upto can be added later without an API break. scheme: 'exact' is hardcoded and only RadiusExactScheme is registered. An optional scheme?: 'exact' | 'upto' on RouteSpec plus a RadiusUptoScheme would fit. Not blocking.

5. Minor: the @x402/express test (test/server.test.ts:258, verify path) covers before mode only. An after-mode case and a failed-handler case would help. Removing RadiusHonoAdapter is a breaking change in a minor bump; fine below 1.0, but worth a line in the changeset.


Generated by Claude Code

@linear-code

linear-code Bot commented Oct 6, 2026

Copy link
Copy Markdown

Consolidating the review items from our discussion:

  1. Preserve streaming in settle-before mode. The review identified await res.arrayBuffer() as buffering the paid response before returning it. Pass the original stream through with settlement headers in before mode, and buffer only where required in after mode. Add a ReadableStream regression test for incremental delivery. This was identified by code inspection, not a live streaming test.
  2. Validate before charging. In settle-before mode, a handler can return 4xx/5xx after payment has moved. Add a pre-payment validation hook or document a clear “validate, then pay” pattern, including an Astro example that returns 404 before charging.
  3. Clarify onSettled semantics. Update the API documentation and README to explain that it records settlement, not successful delivery, and fires before the handler in settle-before mode.
  4. Keep upto as follow-up work. An optional scheme on RouteSpec and a RadiusUptoScheme appear to fit the API; facilitator support should be coordinated with the existing work rather than blocking this PR.
  5. Call out the breaking change. Explicitly note the removal of RadiusHonoAdapter in the changeset and provide migration guidance.
  6. Follow up on developer docs after merge. Lead framework-generic seller examples with radius-sdk/server, retaining Hono as a thin framework wrapper. Update the accept-payments, sell-data, x402, and SDK reference pages alongside the other affected examples. Track this alongside ENG-2346 in Developer Tooling v2.

MCP support is out of scope for this work.

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