Skip to main content

How to Charge Different Prices for the Same Endpoint with x402

A common question from developers building paid APIs with x402 is: how do I charge different prices for the same endpoint based on some criteria? With x402 SDK v2, you can do exactly that by using a custom function for the price (and optionally for the payment recipient) instead of a fixed value. The middleware calls your function on each request and uses the returned price when returning the 402 Payment Required response and when verifying and settling the payment. This article shows how to implement dynamic pricing in every supported stack: TypeScript (Next.js, Express, Hono, and by hand), Python (Flask, FastAPI, and by hand), and Go (Gin and by hand).

The core concept: the pricing function

What makes dynamic pricing possible in x402 is that price (and optionally payTo) can be a function of the request instead of a static value. The server middleware invokes your function on every request and uses the returned price when it sends the 402 Payment Required response and when it verifies and settles the payment. The same idea applies across TypeScript and Python; Go uses a manual approach.

Function signature (TypeScript and Python)

In both TypeScript and Python, the contract is the same:
  • Input: A request context object that gives you access to the incoming HTTP request (headers, query params, path, method, and optionally body).
  • Output: A price for that request—typically a string like "$0.01" or a scheme-specific price object (e.g. AssetAmount in Python for custom tokens).
Optionally, payTo (the recipient address) can also be a function of the same context, so you can route payments differently per request (e.g. by tenant or marketplace split).
LanguagePrice function signatureContext type / adapter
TypeScript(context: HTTPRequestContext) => Price or Promise<Price>context.adapter: getHeader(name), getQueryParam(name), getMethod(), getPath(), getUrl(), optional getBody().
PythonCallable that takes HTTPRequestContext and returns a price (e.g. str or AssetAmount)context.adapter: get_header(name), get_query_param(name), plus method/path/URL. Sync for Flask; sync or async for FastAPI.
GoNo built-in pricing function. You compute the price in your handler (e.g. from query or header), then build the route config or payment requirements for that request yourself before calling the x402 middleware or facilitator.N/A — you read the request (e.g. c.Query("tier"), c.GetHeader("X-Tier")) and pass the result into your config.
Where you plug this in depends on the framework: in TypeScript you pass the function as accepts[].price (and optionally accepts[].payTo) in your route config for Express, Hono, or Next.js. In Python you pass it as PaymentOption(..., price=get_tier_price, ...) in your RouteConfig. The middleware then calls your function when it needs to build the payment requirements for that request. Once you see that, the rest of this article is just framework-specific wiring.

Use case: tier-based pricing

We’ll use one scenario for all examples so you can compare approaches:
  • Endpoint: GET /api/insight (or the framework’s equivalent).
  • Behavior: The client sends a tier via query parameter or header: basic, plus, or pro.
  • Prices:
    • basic → $0.01
    • plus → $0.05
    • pro → $0.10
If no tier is provided, we default to basic. The same pattern works for other criteria (e.g. authenticated user, plan, or custom header).

TypeScript

Next.js

Use withX402 for API routes so payment is settled only after a successful response. The route config can use a function for price (and payTo if needed). The function receives a request context with adapter (headers, query, body, path, method). Example: app/api/insight/route.ts (or under your api directory):
import { NextRequest, NextResponse } from "next/server";
import { withX402 } from "@x402/next";
import { server, paywall, evmAddress, svmAddress } from "../../../proxy";

const TIER_PRICES: Record<string, string> = {
  basic: "$0.01",
  plus: "$0.05",
  pro: "$0.10",
};

function getTierPrice(context: { adapter: { getQueryParam?: (name: string) => string | string[] | undefined; getHeader?: (name: string) => string | undefined } }) {
  const raw = context.adapter.getQueryParam?.("tier") ?? context.adapter.getHeader?.("x-tier");
  const tier = Array.isArray(raw) ? raw[0] : raw;
  return TIER_PRICES[tier ?? "basic"] ?? TIER_PRICES.basic;
}

const handler = async (_: NextRequest) => {
  return NextResponse.json({ insight: "Your tier-based insight here." });
};

export const GET = withX402(
  handler,
  {
    accepts: [
      {
        scheme: "exact",
        price: getTierPrice,
        network: "eip155:84532",
        payTo: evmAddress,
      },
      {
        scheme: "exact",
        price: getTierPrice,
        network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
        payTo: svmAddress,
      },
    ],
    description: "Tier-based insight (basic / plus / pro)",
    mimeType: "application/json",
  },
  server,
  undefined,
  paywall
);
Your proxy.ts (or shared server setup) should export server, paywall, and evmAddress / svmAddress as in the Next.js x402 example.

Express

Use paymentMiddleware with a route config where accepts[].price is a function. The middleware passes a context whose adapter exposes the request (e.g. getQueryParam, getHeader).
import { config } from "dotenv";
import express from "express";
import { paymentMiddleware, x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { ExactSvmScheme } from "@x402/svm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { facilitator } from "@payai/facilitator";

config();

const evmAddress = process.env.EVM_ADDRESS as `0x${string}`;
const svmAddress = process.env.SVM_ADDRESS;
if (!evmAddress || !svmAddress) {
  console.error("Missing required environment variables");
  process.exit(1);
}

const facilitatorClient = new HTTPFacilitatorClient(facilitator);
const resourceServer = new x402ResourceServer(facilitatorClient)
  .register("eip155:84532", new ExactEvmScheme())
  .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme());

const TIER_PRICES: Record<string, string> = {
  basic: "$0.01",
  plus: "$0.05",
  pro: "$0.10",
};

function getTierPrice(context: { adapter: { getQueryParam?: (name: string) => string | string[] | undefined; getHeader?: (name: string) => string | undefined } }) {
  const raw = context.adapter.getQueryParam?.("tier") ?? context.adapter.getHeader?.("tier");
  const tier = Array.isArray(raw) ? raw[0] : raw;
  return TIER_PRICES[tier ?? "basic"] ?? TIER_PRICES.basic;
}

const app = express();

app.use(
  paymentMiddleware(
    {
      "GET /api/insight": {
        accepts: [
          {
            scheme: "exact",
            price: getTierPrice,
            network: "eip155:84532",
            payTo: evmAddress,
          },
          {
            scheme: "exact",
            price: getTierPrice,
            network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
            payTo: svmAddress,
          },
        ],
        description: "Tier-based insight (basic / plus / pro)",
        mimeType: "application/json",
      },
    },
    resourceServer
  )
);

app.get("/api/insight", (req, res) => {
  res.json({ insight: "Your tier-based insight here." });
});

app.listen(4021, () => {
  console.log("Server listening at http://localhost:4021");
});

Hono

Use paymentMiddleware from @x402/hono with a route config where accepts[].price is a function. The middleware passes a context whose adapter exposes the request (e.g. getQueryParam, getHeader). Attach the middleware with app.use(), then define your route; run the server with serve({ fetch: app.fetch, port }).
import { config } from "dotenv";
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { paymentMiddleware, x402ResourceServer } from "@x402/hono";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { ExactSvmScheme } from "@x402/svm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";
import { facilitator } from "@payai/facilitator";

config();

const evmAddress = process.env.EVM_ADDRESS as `0x${string}`;
const svmAddress = process.env.SVM_ADDRESS;
if (!evmAddress || !svmAddress) {
  console.error("Missing required environment variables");
  process.exit(1);
}

const facilitatorClient = new HTTPFacilitatorClient(facilitator);
const resourceServer = new x402ResourceServer(facilitatorClient)
  .register("eip155:84532", new ExactEvmScheme())
  .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme());

const TIER_PRICES: Record<string, string> = {
  basic: "$0.01",
  plus: "$0.05",
  pro: "$0.10",
};

function getTierPrice(context: { adapter: { getQueryParam?: (name: string) => string | string[] | undefined; getHeader?: (name: string) => string | undefined } }) {
  const raw = context.adapter.getQueryParam?.("tier") ?? context.adapter.getHeader?.("tier");
  const tier = Array.isArray(raw) ? raw[0] : raw;
  return TIER_PRICES[tier ?? "basic"] ?? TIER_PRICES.basic;
}

const app = new Hono();

app.use(
  paymentMiddleware(
    {
      "GET /api/insight": {
        accepts: [
          {
            scheme: "exact",
            price: getTierPrice,
            network: "eip155:84532",
            payTo: evmAddress,
          },
          {
            scheme: "exact",
            price: getTierPrice,
            network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
            payTo: svmAddress,
          },
        ],
        description: "Tier-based insight (basic / plus / pro)",
        mimeType: "application/json",
      },
    },
    resourceServer
  )
);

app.get("/api/insight", (c) => c.json({ insight: "Your tier-based insight here." }));

serve({ fetch: app.fetch, port: 4021 });
console.log("Server listening at http://localhost:4021");

Custom (by hand)

The important part is that RouteConfig.accepts[].price (and optionally payTo) can be a function (context: HTTPRequestContext) => Price | Promise<Price>. The HTTP resource server calls it when building the payment requirements for that request. “By hand” you wire the same x402HTTPResourceServer and dynamic route config into your own HTTP stack instead of using Express, Hono, or Next.js. Without one of those frameworks, you can still use the same dynamic price support from the SDK:
  1. Create an x402ResourceServer and register schemes (EVM, SVM, etc.).
  2. Create an x402HTTPResourceServer from @x402/core/server with a single route whose accepts[].price is your getTierPrice function (and optionally payTo as a function).
  3. Use paymentMiddlewareFromHTTPServer(httpServer) from @x402/express (or the Hono equivalent) to plug that HTTP server into any stack that can run the middleware, or drive it yourself: on each request, build an HTTPRequestContext with an adapter that implements getHeader, getMethod, getPath, getUrl, and optionally getQueryParam / getBody, then call httpServer.processHTTPRequest(context) and handle the result (402, payment-verified, or no-payment-required).
  4. If the result is payment-verified, run your handler, then call httpServer.processSettlement(...) with the captured payload and requirements.

Python

Flask

Use payment_middleware with a RouteConfig whose PaymentOption.price is a callable that receives HTTPRequestContext. The context has adapter (e.g. get_query_param, get_header). Use the sync server and sync callables for Flask.
import os
from dotenv import load_dotenv
from flask import Flask, jsonify
from x402.http import FacilitatorConfig, HTTPFacilitatorClientSync, PaymentOption
from x402.http.middleware.flask import payment_middleware
from x402.http.types import HTTPRequestContext, RouteConfig
from x402.mechanisms.evm.exact import ExactEvmServerScheme
from x402.mechanisms.svm.exact import ExactSvmServerScheme
from x402.schemas import Network
from x402.server import x402ResourceServerSync

load_dotenv()

EVM_ADDRESS = os.getenv("EVM_ADDRESS")
SVM_ADDRESS = os.getenv("SVM_ADDRESS")
EVM_NETWORK: Network = "eip155:84532"
SVM_NETWORK: Network = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"
FACILITATOR_URL = os.getenv("FACILITATOR_URL", "https://facilitator.payai.network")

if not EVM_ADDRESS or not SVM_ADDRESS:
    raise ValueError("Missing required environment variables")

app = Flask(__name__)

facilitator = HTTPFacilitatorClientSync(FacilitatorConfig(url=FACILITATOR_URL))
server = x402ResourceServerSync(facilitator)
server.register(EVM_NETWORK, ExactEvmServerScheme())
server.register(SVM_NETWORK, ExactSvmServerScheme())

TIER_PRICES = {"basic": "$0.01", "plus": "$0.05", "pro": "$0.10"}


def get_tier_price(context: HTTPRequestContext) -> str:
    tier = context.adapter.get_query_param("tier") or context.adapter.get_header("x-tier") or "basic"
    if isinstance(tier, list):
        tier = tier[0] if tier else "basic"
    return TIER_PRICES.get(tier, TIER_PRICES["basic"])


routes = {
    "GET /api/insight": RouteConfig(
        accepts=[
            PaymentOption(
                scheme="exact",
                pay_to=EVM_ADDRESS,
                price=get_tier_price,
                network=EVM_NETWORK,
            ),
            PaymentOption(
                scheme="exact",
                pay_to=SVM_ADDRESS,
                price=get_tier_price,
                network=SVM_NETWORK,
            ),
        ],
        mime_type="application/json",
        description="Tier-based insight (basic / plus / pro)",
    ),
}
payment_middleware(app, routes=routes, server=server)


@app.route("/api/insight")
def get_insight():
    return jsonify({"insight": "Your tier-based insight here."})


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=4021, debug=False)

FastAPI

Use PaymentMiddlewareASGI from x402.http.middleware.fastapi with a RouteConfig whose PaymentOption.price is a callable that receives HTTPRequestContext. The context has adapter (e.g. get_query_param, get_header). Use the async HTTP resource server; your price callable can be sync or async—the server will await it when building payment requirements.
import os
from dotenv import load_dotenv
from fastapi import FastAPI
from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption
from x402.http.middleware.fastapi import PaymentMiddlewareASGI
from x402.http.types import HTTPRequestContext, RouteConfig
from x402.mechanisms.evm.exact import ExactEvmServerScheme
from x402.mechanisms.svm.exact import ExactSvmServerScheme
from x402.schemas import Network
from x402.server import x402ResourceServer

load_dotenv()

EVM_ADDRESS = os.getenv("EVM_ADDRESS")
SVM_ADDRESS = os.getenv("SVM_ADDRESS")
EVM_NETWORK: Network = "eip155:84532"
SVM_NETWORK: Network = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1"
FACILITATOR_URL = os.getenv("FACILITATOR_URL", "https://facilitator.payai.network")

if not EVM_ADDRESS or not SVM_ADDRESS:
    raise ValueError("Missing required environment variables")

app = FastAPI()

facilitator = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL))
server = x402ResourceServer(facilitator)
server.register(EVM_NETWORK, ExactEvmServerScheme())
server.register(SVM_NETWORK, ExactSvmServerScheme())

TIER_PRICES = {"basic": "$0.01", "plus": "$0.05", "pro": "$0.10"}


def get_tier_price(context: HTTPRequestContext) -> str:
    tier = context.adapter.get_query_param("tier") or context.adapter.get_header("x-tier") or "basic"
    if isinstance(tier, list):
        tier = tier[0] if tier else "basic"
    return TIER_PRICES.get(tier, TIER_PRICES["basic"])


routes = {
    "GET /api/insight": RouteConfig(
        accepts=[
            PaymentOption(
                scheme="exact",
                pay_to=EVM_ADDRESS,
                price=get_tier_price,
                network=EVM_NETWORK,
            ),
            PaymentOption(
                scheme="exact",
                pay_to=SVM_ADDRESS,
                price=get_tier_price,
                network=SVM_NETWORK,
            ),
        ],
        mime_type="application/json",
        description="Tier-based insight (basic / plus / pro)",
    ),
}
app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server)


@app.get("/api/insight")
async def get_insight():
    return {"insight": "Your tier-based insight here."}


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=4021)

Custom (by hand)

The important part is that PaymentOption.price (and optionally pay_to) can be a callable that takes HTTPRequestContext and returns a price (e.g. a string like "$0.01" or an AssetAmount). The context’s adapter exposes the request (e.g. get_query_param, get_header). The HTTP resource server calls your callable when building the payment requirements for that request. “By hand” you wire the same x402HTTPResourceServer (or sync variant) and dynamic route config into your own stack instead of using Flask or FastAPI middleware. Without that middleware, you can still use the same dynamic price support:
  1. Build an x402ResourceServer (sync or async) and register schemes.
  2. Build an x402HTTPResourceServer or x402HTTPResourceServerSync with a single route whose PaymentOption.price is your get_tier_price(context) callable.
  3. For each request, wrap the request in the framework’s adapter, build an HTTPRequestContext, and call http_server.process_http_request(context) (or the async variant).
  4. If the result is payment-verified, run your handler, then call process_settlement with the returned payload and requirements.

Go

Gin

The Go x402 HTTP layer currently expects static route configs (no function type for price in the public API). You can still implement tier-based pricing in two ways: Option A – Multiple route patterns (static configs)
Register separate route patterns per tier, e.g. GET /api/insight/basic, GET /api/insight/plus, GET /api/insight/pro, each with its own fixed price in RoutesConfig. The client then calls the path that matches their tier. This is “different prices” by using different routes, not one endpoint with a dynamic function.
Option B – One route, dynamic price by hand
Use a single route that does not go through the middleware’s static route table for payment. In the handler, read tier from the request, compute the price, build payment requirements (and optionally call the facilitator’s verify/settle), and return 402 or proceed. That means you implement the 402 flow yourself for that route while still using the same facilitator and schemes elsewhere.
Example sketch for Option B with Gin: in a handler, get the tier from query or header, map it to a price string, build a RouteConfig-like struct for that request, then use the Go HTTP server’s low-level APIs (if exposed) to run verify/settle with that config. The Gin example uses static RoutesConfig; for true per-request price you’d call the resource server’s verify/settle with requirements you build from the current request’s tier.

Custom (by hand)

Fully by hand in Go you:
  1. Parse the request (path, method, query, headers) and decide the price (e.g. from tier).
  2. Build the payment requirements (scheme, network, payTo, price, etc.) for that request.
  3. If there is no valid PAYMENT-SIGNATURE (or equivalent), respond with 402 and the PAYMENT-REQUIRED body/header.
  4. If the client sends a payment, call the facilitator’s verify endpoint with the payload and your requirements; if valid, run your business logic, then call settle, and return the resource with the settlement response.
So in Go, “custom” dynamic pricing is: compute price per request, build requirements, then drive verify/settle yourself. The x402 reference and facilitator docs describe the message shapes and endpoints.

Summary

StackHow to get different prices for the same endpoint
TS (Next/Express/Hono)Use a function for accepts[].price (and optionally payTo) in the route config. The middleware passes a request context; your function returns the price for that request.
Python (Flask/FastAPI)Use a callable for PaymentOption.price (and optionally pay_to) that takes HTTPRequestContext. Sync for Flask (sync server), sync or async for FastAPI (async server).
Go (Gin)Use multiple static routes (different paths per tier) or implement one route “by hand”: compute price from the request, build requirements, then verify/settle via the facilitator.
The TypeScript and Python SDKs support dynamic price (and payTo) functions out of the box; in Go you achieve the same behavior by building requirements per request and using the low-level verify/settle flow. For more on x402, see the x402 introduction, x402 reference, and server quickstarts: Express, Hono, Next.js, Flask, FastAPI, Gin.

Still looking for more?

Check out the Coinbase x402 repository for the official protocol spec, SDK source, and additional examples.