diff --git a/example-x402/.env.example b/example-x402/.env.example
new file mode 100644
index 00000000..ff777562
--- /dev/null
+++ b/example-x402/.env.example
@@ -0,0 +1,5 @@
+# Stripe — fresh crypto deposit addresses are minted via Stripe PaymentIntents
+STRIPE_SECRET_KEY=sk_test_...
+
+# Optional — defaults to https://www.x402.org/facilitator
+FACILITATOR_URL=https://www.x402.org/facilitator
diff --git a/example-x402/.gitignore b/example-x402/.gitignore
new file mode 100644
index 00000000..b1640b50
--- /dev/null
+++ b/example-x402/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+dist
+.env
+.env.local
diff --git a/example-x402/README.md b/example-x402/README.md
new file mode 100644
index 00000000..dc2629b6
--- /dev/null
+++ b/example-x402/README.md
@@ -0,0 +1,76 @@
+# example-x402
+
+A spiceflow app that protects a single API route (`GET /paid`) with the
+[x402](https://www.x402.org) payment protocol. Each request costs **$0.01**,
+settled on **Base Sepolia** in USDC. Fresh deposit addresses are minted via
+Stripe PaymentIntents, mirroring [stripe-samples/machine-payments](https://github.com/stripe-samples/machine-payments).
+
+## Shape
+
+```
+app (root Spiceflow)
+├── serveStatic('/public')
+├── layout '/*'
+├── page '/' ← RSC page with a client comp
+└── use(paidApp)
+ paidApp (Spiceflow sub-app)
+ ├── use(x402({ price, network, payTo })) ← only runs for paidApp routes
+ └── get '/paid' → () => ({ foo: 'bar' })
+```
+
+Spiceflow scopes middleware by **sub-app ownership**, not by path. Mounting
+`paidApp` via `app.use(paidApp)` means the x402 handshake only runs for routes
+defined on `paidApp`. Anything on the parent app (`/`, static files, RSC
+pages) is untouched.
+
+## Why a custom middleware instead of `@x402/hono`
+
+`@x402/hono`'s `paymentMiddleware` is ~380 lines and wraps a ~1000-line
+`x402HTTPResourceServer`. It carries its own route matcher, HTML paywall
+generator, Hono context adapter, bazaar extension loader, and settlement
+overrides header. Spiceflow already has a trie router, so a bespoke ~180-line
+middleware at [`src/x402-middleware.ts`](./src/x402-middleware.ts) is enough.
+It only imports from `@x402/core/server`, `@x402/core/http`, `@x402/core/types`,
+and `@x402/evm/exact/server`.
+
+## Setup
+
+```bash
+cp .env.example .env
+# fill in STRIPE_SECRET_KEY
+pnpm install
+pnpm dev
+```
+
+Open and click **Call /paid** to see the 402 challenge,
+or hit the route directly with curl:
+
+```bash
+curl -i http://localhost:3000/paid
+# HTTP/1.1 402 Payment Required
+# www-authenticate: x402 scheme="exact"
+# content-type: application/json
+#
+# {"x402Version":2,"error":"Payment required","accepts":[...]}
+```
+
+A real x402 client would sign an on-chain payment to the `payTo` address in
+the challenge and retry with an `x-payment` header. See
+[x402.org](https://www.x402.org) for client libraries.
+
+## Environment
+
+| Variable | Required | Default |
+|---------------------|----------|-------------------------------------------|
+| `STRIPE_SECRET_KEY` | yes | — |
+| `FACILITATOR_URL` | no | `https://www.x402.org/facilitator` |
+| `PORT` | no | `3000` |
+
+## Tests
+
+```bash
+pnpm test
+```
+
+The middleware unit tests inject a fake `x402ResourceServer` via the
+undocumented `server` option on `x402()`. No network, no mocks.
diff --git a/example-x402/package.json b/example-x402/package.json
new file mode 100644
index 00000000..75b14950
--- /dev/null
+++ b/example-x402/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "example-x402",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "start": "node dist/rsc/index.js",
+ "test": "vitest --run"
+ },
+ "dependencies": {
+ "@tailwindcss/vite": "^4.2.2",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "19.2.3",
+ "@x402/core": "^2.9.0",
+ "@x402/evm": "^2.9.0",
+ "dotenv": "^17.4.0",
+ "errore": "^0.14.0",
+ "node-cache": "^5.1.2",
+ "react": "19.2.4",
+ "react-dom": "19.2.4",
+ "spiceflow": "workspace:^",
+ "stripe": "^21.0.1",
+ "tailwindcss": "4.0.6",
+ "typescript": "5.7.3",
+ "vite": "^8.0.8"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^6.0.1",
+ "vitest": "^4.1.4"
+ }
+}
diff --git a/example-x402/public/favicon.ico b/example-x402/public/favicon.ico
new file mode 100644
index 00000000..718d6fea
Binary files /dev/null and b/example-x402/public/favicon.ico differ
diff --git a/example-x402/src/globals.css b/example-x402/src/globals.css
new file mode 100644
index 00000000..0f4e93b6
--- /dev/null
+++ b/example-x402/src/globals.css
@@ -0,0 +1,19 @@
+@import 'tailwindcss';
+
+:root {
+ --foreground-rgb: 0, 0, 0;
+ --background-rgb: 255, 255, 255;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-rgb: 10, 10, 10;
+ }
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: rgb(var(--background-rgb));
+ font-family: system-ui, -apple-system, sans-serif;
+}
diff --git a/example-x402/src/main.tsx b/example-x402/src/main.tsx
new file mode 100644
index 00000000..d9869470
--- /dev/null
+++ b/example-x402/src/main.tsx
@@ -0,0 +1,87 @@
+// Spiceflow + x402 example.
+//
+// Shape:
+// app (root)
+// ├── serveStatic('/public')
+// ├── layout '/*' — RSC layout
+// ├── page '/' — RSC page with a client component
+// └── use(paidApp)
+// paidApp (sub-app)
+// ├── use(x402({...})) ← only applies to routes on paidApp
+// └── get '/paid' → () => ({ foo: 'bar' })
+//
+// Request flow:
+// GET / → rootApp.page('/'), no x402 middleware in chain, renders HTML
+// GET /paid → paidApp.get('/paid'), x402 middleware enforces payment
+import './globals.css'
+import { Spiceflow, serveStatic } from 'spiceflow'
+import { Head, Link } from 'spiceflow/react'
+import Stripe from 'stripe'
+import { x402, stripeDepositAddress } from './x402-middleware.js'
+import { PaidData } from './paid-data.js'
+
+const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? 'sk_test_placeholder')
+
+// Sub-app that owns every x402-protected route. Middleware registered on
+// paidApp only runs for routes defined on paidApp (see
+// getAppsInScope in spiceflow.tsx), so `/` below is untouched.
+export const paidApp = new Spiceflow()
+ .use(
+ x402({
+ price: '$0.01',
+ network: 'eip155:84532', // Base Sepolia
+ facilitatorUrl:
+ process.env.FACILITATOR_URL ?? 'https://www.x402.org/facilitator',
+ payTo: stripeDepositAddress({ stripe, network: 'base' }),
+ }),
+ )
+ .get('/paid', () => ({ foo: 'bar' }))
+
+export const app = new Spiceflow()
+ .use(serveStatic({ root: './public' }))
+ .use(paidApp)
+ .layout('/*', ({ children }) => (
+ {children}
+ ))
+ .page('/', function Home() {
+ return (
+
+
x402 + spiceflow
+
+ /paid is protected by the x402
+ payment protocol. Each call costs $0.01, settled on
+ Base Sepolia, with a fresh deposit address minted via Stripe.
+
+
+
+
+ Hit it directly to see the 402 challenge:{' '}
+
+ /paid
+
+
+
+ Or from a shell:{' '}
+ curl -i http://localhost:3000/paid
+
+
+
+ )
+ })
+
+function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ x402 + spiceflow
+
+ {children}
+
+ )
+}
+
+void app.listen(Number(process.env.PORT || 3000))
+
+declare module 'spiceflow/react' {
+ interface SpiceflowRegister { app: typeof app }
+}
diff --git a/example-x402/src/paid-data.tsx b/example-x402/src/paid-data.tsx
new file mode 100644
index 00000000..3a11a2eb
--- /dev/null
+++ b/example-x402/src/paid-data.tsx
@@ -0,0 +1,90 @@
+'use client'
+
+// Client component that demonstrates the x402 handshake from the browser.
+// The first fetch gets a 402 with the payment challenge; the real "agent"
+// flow would then sign an on-chain payment and retry with an x-payment
+// header. We just show the challenge body so you can see it.
+import { useState } from 'react'
+
+interface Challenge {
+ x402Version: number
+ error: string
+ accepts: Array<{
+ scheme: string
+ network: string
+ payTo: string
+ amount: string
+ asset?: string
+ }>
+}
+
+export function PaidData() {
+ const [state, setState] = useState<
+ | { kind: 'idle' }
+ | { kind: 'loading' }
+ | { kind: 'challenge'; status: number; body: Challenge }
+ | { kind: 'paid'; body: unknown }
+ | { kind: 'error'; message: string }
+ >({ kind: 'idle' })
+
+ return (
+