A complete example of a server that charges different amounts based on query parameters, using handleMiddlewareRequest from @faremeter/middleware/common.
createMiddleware accepts static payment requirements at initialization, so the price is fixed per route. When you need to compute requirements per-request — for example, varying the amount based on a query parameter — use handleMiddlewareRequest from @faremeter/middleware/common. The tradeoff is that you wire up the framework integration yourself: reading headers, sending JSON responses, and calling your route handler.
Unlike fixed pricing per route, dynamic pricing lets you derive payment requirements from any aspect of the incoming request: headers, query parameters, path, or body. You can also vary:
- The networks, schemes, and assets accepted
- The
payTo wallet address
- The facilitator URL
Dynamic payment requirements must be idempotent. The same request must produce the same requirements every time, because the client may retry payment against the same requirements.
Server
import { Hono, type MiddlewareHandler } from "hono"
import { HTTPException } from "hono/http-exception"
import { serve } from "@hono/node-server"
import { x402Exact as solanaX402Exact } from "@faremeter/info/solana"
import { x402Exact } from "@faremeter/info/evm"
import {
handleMiddlewareRequest,
createPaymentRequiredResponseCache,
resolveSupportedVersions,
} from "@faremeter/middleware/common"
const createDynamicPricingMiddleware = (): MiddlewareHandler => {
const { getPaymentRequiredResponse } = createPaymentRequiredResponseCache({})
return async (c, next) => {
const amount = c.req.query("amount")
const asset = "USDC"
// In production, validate that amount is a numeric string.
if (amount === undefined) {
throw new HTTPException(400, {
message: "you need to provide an 'amount' querystring parameter",
})
}
const accepts = [
...solanaX402Exact({
network: "devnet",
asset,
amount,
payTo: process.env.MERCHANT_ADDRESS,
}),
x402Exact({
network: "base-sepolia",
asset,
amount,
payTo: process.env.EVM_MERCHANT_ADDRESS,
}),
]
return await handleMiddlewareRequest({
facilitatorURL: "https://facilitator.corbits.dev",
accepts,
supportedVersions: resolveSupportedVersions(),
resource: c.req.url,
getHeader: (key) => c.req.header(key),
getPaymentRequiredResponse,
sendJSONResponse: (status, body, headers) => {
if (headers) {
for (const [key, value] of Object.entries(headers)) {
c.header(key, value)
}
}
c.status(status)
return c.json(body)
},
body: async ({ settle }) => {
const settleResult = await settle()
if (!settleResult.success) {
return settleResult.errorResponse
}
// On success, call the next handler and let Hono set the response.
await next()
},
})
}
}
const app = new Hono()
app.get("/protected", createDynamicPricingMiddleware(), (c) => {
return c.json({ msg: "success" })
})
// @hono/node-server is required to run Hono as a standalone Node.js server.
serve(app, (info) => {
console.log(`Listening on http://localhost:${info.port}`)
})
Client
import "dotenv/config"
import { payer } from "@faremeter/rides"
import { getLogger } from "@faremeter/logs"
const logger = await getLogger(["client"])
await payer.addLocalWallet(process.env.PAYER_KEYPAIR_PATH)
await payer.addLocalWallet(process.env.EVM_PRIVATE_KEY)
const response = await payer.fetch("http://localhost:3000/protected?amount=1000")
logger.info(JSON.stringify(await response.json()))
Running the example
# Terminal 1
MERCHANT_ADDRESS=7xKXwxRPMo2sUAT5... EVM_MERCHANT_ADDRESS=0x1234... pnpm tsx server.ts
# Terminal 2
PAYER_KEYPAIR_PATH=~/.config/solana/id.json EVM_PRIVATE_KEY=0xabc... pnpm tsx client.ts
The client requests /protected?amount=1000, and the server returns payment requirements for 1000 units (0.001 USDC). Changing the amount query parameter changes the price charged.