@polymerdao/x402-json-rpc-middleware
v0.3.8
Published
x402 payment middleware for JSON RPC endpoints on Hono framework
Readme
x402 JSON RPC Middleware
Payment middleware for JSON RPC endpoints on the Hono framework, built on top of the x402 Payment Protocol.
Overview
This middleware enables monetization of JSON RPC APIs by requiring cryptocurrency payments for specific RPC methods. Unlike the standard x402-hono middleware which works with HTTP routes, this middleware is specifically designed for JSON RPC endpoints where all requests go to a single HTTP endpoint but contain different method names in the request body.
Features
- Method-based Pricing: Configure pricing per JSON RPC method (e.g.,
eth_call,eth_getBalance) - Static & Dynamic Pricing: Set fixed prices or calculate dynamically based on request context
- Regex Pattern Matching: Use patterns like
debug_.*to match multiple methods - Batch Request Support: Handle JSON RPC batch requests with aggregated pricing
- Multiple Networks: Support payment on multiple blockchain networks per method
- Custom Facilitators: Configure different facilitators per network or method
- Multiple Payment Tokens: Accept different tokens for payment
- Implicit Allow: Unconfigured methods pass through without payment (configurable)
- Full x402 Integration: Payment verification, settlement, and paywall UI
Installation
npm install x402-json-rpc-middleware
# or
pnpm add x402-json-rpc-middleware
# or
yarn add x402-json-rpc-middlewareQuick Start
import { Hono } from "hono";
import { jsonRpcPaymentMiddleware } from "x402-json-rpc-middleware";
const app = new Hono();
app.use(
"/rpc",
jsonRpcPaymentMiddleware({
payTo: "0xYourAddressHere",
methods: {
// Static pricing
eth_call: {
price: { amount: "1000" }, // In smallest unit (e.g., 0.001 USDC)
networks: [{ network: "base-sepolia" }],
},
// Regex patterns
"debug_.*": {
price: { amount: "5000" },
networks: [{ network: "base-sepolia" }],
},
// Dynamic pricing
eth_getLogs: {
price: (context) => {
// Calculate based on request
return { amount: "2000" };
},
networks: [{ network: "base-sepolia" }],
},
},
facilitator: {
url: "https://x402-facilitator.coinbase.com",
},
}),
);
app.post("/rpc", async (c) => {
// Your JSON RPC handler
const body = await c.req.json();
// Process and return response
});Configuration
JsonRpcMiddlewareConfig
interface JsonRpcMiddlewareConfig {
// Recipient address for payments
payTo: `0x${string}` | string;
// Method configurations
methods: {
[methodPattern: string]: JsonRpcMethodConfig;
};
// Global facilitator (optional)
facilitator?: FacilitatorConfig;
// Paywall UI configuration (optional)
paywall?: PaywallConfig;
// Logger for debugging and monitoring (optional)
logger?: Logger;
}JsonRpcMethodConfig
interface JsonRpcMethodConfig {
// Static price or dynamic pricing function
price: Money | ((context: Context) => Money | Promise<Money>);
// Networks this method is available on
networks: NetworkConfig[];
// Optional description
description?: string;
// Optional timeout in seconds (default: 60)
maxTimeoutSeconds?: number;
}NetworkConfig
interface NetworkConfig {
// Network identifier
network: Network; // e.g., "base-sepolia", "ethereum-mainnet"
// Optional custom facilitator for this network
facilitator?: FacilitatorConfig;
// Optional allowed payment tokens
tokens?: Array<{
address: `0x${string}`;
decimals: number;
name: string;
symbol: string;
}>;
}Method Pattern Matching
The middleware supports three types of method patterns:
1. Exact Match
methods: {
"eth_call": { /* config */ },
"eth_getBalance": { /* config */ }
}2. Simple Regex
methods: {
"debug_.*": { /* matches debug_traceTransaction, debug_traceCall, etc. */ },
"trace_.*": { /* matches all trace methods */ }
}3. Full Regex Pattern
methods: {
"/^(eth_getLogs|eth_getFilterLogs)$/": { /* matches both methods */ },
"/^eth_(call|estimateGas)$/": { /* matches eth_call and eth_estimateGas */ }
}Dynamic Pricing
Dynamic pricing allows you to calculate the price based on the request context:
methods: {
eth_call: {
price: async (context) => {
const body = await context.req.json();
// Calculate based on request parameters
const complexity = calculateComplexity(body.params);
const basePrice = BigInt(1000);
const finalPrice = basePrice * BigInt(complexity);
return {
amount: finalPrice.toString(),
asset: {
address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
decimals: 6,
eip712: { name: "USDC", version: "1" }
}
};
},
networks: [{ network: "base-sepolia" }]
}
}Multiple Networks
Allow clients to pay on their preferred network:
methods: {
eth_getBalance: {
price: { amount: "500" },
networks: [
{
network: "base-sepolia",
// Uses global facilitator
},
{
network: "base-mainnet",
// Custom facilitator for mainnet
facilitator: {
url: "https://mainnet-facilitator.example.com"
}
},
{
network: "ethereum-sepolia",
// Different tokens accepted
tokens: [
{
address: "0x...",
decimals: 6,
name: "USDC",
symbol: "USDC"
}
]
}
]
}
}Logging
The middleware supports custom logging for debugging and monitoring. By default, the middleware runs silently (no logs). You can provide your own logger implementation that works with your deployment environment.
Logger Interface
interface Logger {
info(message: string, ...args: unknown[]): void;
info(obj: Record<string, unknown>, message: string): void;
warn(message: string, ...args: unknown[]): void;
warn(obj: Record<string, unknown>, message: string): void;
error(message: string, ...args: unknown[]): void;
error(obj: Record<string, unknown>, message: string): void;
debug(message: string, ...args: unknown[]): void;
debug(obj: Record<string, unknown>, message: string): void;
}Configuration Examples
Node.js with Console
const logger = {
info: (msg: string) => console.log('[INFO]', msg),
warn: (msg: string) => console.warn('[WARN]', msg),
error: (msg: string) => console.error('[ERROR]', msg),
debug: (msg: string) => console.debug('[DEBUG]', msg),
};
app.use("/rpc", jsonRpcPaymentMiddleware({
payTo: "0x...",
methods: { /* ... */ },
logger,
}));Node.js with Pino
import pino from 'pino';
const logger = pino({
level: 'info',
transport: {
target: 'pino-pretty'
}
});
app.use("/rpc", jsonRpcPaymentMiddleware({
payTo: "0x...",
methods: { /* ... */ },
logger,
}));Cloudflare Workers
// Simple console wrapper for Workers
const logger = {
info: (msg: string) => console.log(msg),
warn: (msg: string) => console.warn(msg),
error: (msg: string) => console.error(msg),
debug: (msg: string) => console.debug(msg),
};
// Or use Workers-compatible logging services
// like Axiom, Logtail, Baselime, etc.What Gets Logged
The middleware logs:
- Warnings: Invalid network configurations, failed payment requirement builds
- Errors: Middleware errors, payment verification failures
- Info: (Optional) Payment flow events when implemented
Note: The default silent logger ensures production safety. Enable logging only when needed for debugging or monitoring.
Batch Requests
The middleware automatically handles JSON RPC batch requests:
POST /rpc
[
{"jsonrpc": "2.0", "method": "eth_call", "params": [], "id": 1},
{"jsonrpc": "2.0", "method": "eth_getBalance", "params": ["0x..."], "id": 2}
]- Prices are aggregated across all paid methods
- All methods must use compatible payment tokens
- Unconfigured methods in the batch are free (implicit allow)
Payment Flow
1. Initial Request (No Payment)
Client sends JSON RPC request without payment:
curl -X POST http://localhost:3000/rpc \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "method": "eth_call", "params": [], "id": 1}'Server responds with 402 Payment Required (following x402 protocol):
{
"x402Version": 1,
"accepts": [
{
"scheme": "exact",
"network": "base-sepolia",
"maxAmountRequired": "1000",
"asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"payTo": "0x...",
"resource": "http://localhost:3000/rpc",
"description": "JSON RPC call: eth_call",
"mimeType": "application/json"
}
],
"totalPrice": {
"amount": "1000"
},
"breakdown": [
{
"method": "eth_call",
"price": { "amount": "1000" }
}
]
}Note: The response follows the official x402 protocol format with x402Version and accepts fields. The totalPrice and breakdown fields are additional metadata specific to the JSON-RPC context.
2. Request with Payment
Client includes payment in X-PAYMENT header:
curl -X POST http://localhost:3000/rpc \
-H "Content-Type: application/json" \
-H "X-PAYMENT: <payment_token>" \
-d '{"jsonrpc": "2.0", "method": "eth_call", "params": [], "id": 1}'Server verifies payment and returns result:
{
"jsonrpc": "2.0",
"result": "0x...",
"id": 1
}Response includes X-PAYMENT-RESPONSE header for settlement.
Error Codes
The middleware uses standard JSON RPC 2.0 error codes for request processing errors:
| Code | Message | Description | |------|---------|-------------| | -32700 | Parse error | Invalid JSON | | -32600 | Invalid Request | Invalid JSON RPC 2.0 format | | -32603 | Internal error | Server error |
Note: Payment-related responses use HTTP 402 status code (not JSON-RPC errors), following the x402 protocol specification. The 402 response contains payment requirements in the body as shown in the Payment Flow section above.
Examples
See the examples directory for complete working examples:
- Basic Example: Static pricing, regex patterns, single network
- Advanced Example: Dynamic pricing, multiple networks, custom facilitators
API Reference
Main Function
jsonRpcPaymentMiddleware(config: JsonRpcMiddlewareConfig): MiddlewareHandler
Creates a Hono middleware handler for JSON RPC payment enforcement.
Utility Functions
parseJsonRpcRequest(body: unknown): JsonRpcRequestInput
Parses and validates a JSON RPC 2.0 request.
calculatePrice(matchedMethods: MatchedMethod[], context: Context): Promise<PriceCalculation>
Calculates total price for a set of matched methods.
MethodMatcher
Class for matching JSON RPC methods against configured patterns.
const matcher = new MethodMatcher(methodsConfig);
const match = matcher.match("eth_call");Types
All TypeScript types are exported for use in your application:
import type {
JsonRpcMiddlewareConfig,
JsonRpcMethodConfig,
NetworkConfig,
DynamicPriceFn,
Money,
// ... and more
} from "x402-json-rpc-middleware";Best Practices
1. Start with Static Pricing
Begin with static prices and add dynamic pricing only when needed:
methods: {
"eth_call": { price: { amount: "1000" }, networks: [...] }
}2. Use Regex Patterns for Method Groups
Group similar methods with regex patterns:
methods: {
"debug_.*": { price: { amount: "5000" }, networks: [...] },
"trace_.*": { price: { amount: "10000" }, networks: [...] }
}3. Provide Multiple Networks
Give users flexibility to pay on their preferred network:
networks: [
{ network: "base-sepolia" }, // Testnet
{ network: "base-mainnet" }, // Mainnet
{ network: "ethereum-mainnet" } // Alternative
]4. Monitor Performance
Dynamic pricing functions run on every request. Keep them fast:
// Good - Quick calculation
price: (c) => {
const multiplier = parseInt(c.req.query("tier") || "1");
return { amount: (1000 * multiplier).toString() };
}
// Avoid - Slow external calls
price: async (c) => {
// Don't do this on every request
const price = await fetchPriceFromDatabase();
return price;
}5. Set Appropriate Timeouts
Complex operations may need longer timeouts:
methods: {
"trace_block": {
price: { amount: "10000" },
networks: [...],
maxTimeoutSeconds: 120 // 2 minutes for complex traces
}
}Development
# Install dependencies
pnpm install
# Build
pnpm build
# Type check
pnpm typecheck
# Lint
pnpm lint
# Format
pnpm format
# Run examples
pnpm example:basic
pnpm example:advancedLicense
Apache-2.0
