Skip to main content

Implementing Dynamic Pricing

Unlike fixed x402 pricing per route, Dynamic Pricing allows you to return payment requirements to clients, based arbitrarily on the request they’re making. All aspects of the client request can be used to make pricing decisions (e.g. headers, query parameters, paths, body). It is also possible to make queries to external resources (e.g. databases) to augment pricing information. In addition to the price charged, it’s also possible to provide dynamic:
  • Networks/Schemes/Assets (e.g. Solana USDC can be accepted for only certain routes, but Monad on others).
  • payTo can be made dynamic (e.g. some routes use a specific incoming wallet, others use a different wallet).
  • The facilitator being used can be dynamic as well (e.g. some routes use a higher priority facilitator than others).
The options are unlimited. The only hard requirement is that the generated payment requirements MUST be idempotent (i.e. the same requirements must be sent if the request is the same).

Hono Middleware Example

The example provided uses TypeScript and Hono using @faremeter/middleware. All of the techniques described in this document can be implemented using other frameworks (e.g. Express), and other languages. Fundamentally, the example does the following:
  1. Import Hono and Faremeter packages for use (in this case, with node).
  2. Create a custom middleware that generates dynamic pricing based on the query string.
  3. If the client attempts to pay, try to settle the payment before proceeding to handling the actual request.
  4. Attach the custom middleware to a /protected route, that returns a JSON object.
import { Hono, type MiddlewareHandler } from "hono";
import { HTTPException } from "hono/http-exception";
import { serve } from "@hono/node-server";
import { solana, evm } from "@faremeter/info";
import {
  handleMiddlewareRequest,
  createPaymentRequiredResponseCache,
} from "@faremeter/middleware/common";

const createMiddleware = (): MiddlewareHandler => {
  // Use requirements caching, so we don't have to query the
  // facilitator if requirements don't change.
  const { getPaymentRequiredResponse } = createPaymentRequiredResponseCache({});

  return async (c, next) => {
    const amount = c.req.query("amount");
    const asset = "USDC";

    // Look at the request, to determine how much we should charge.
    if (amount === undefined) {
      throw new HTTPException(400, {
        message: "you need to provide an 'amount' querystring parameter",
      });
    }

    // The amount will be divided by the decimals for the asset.
    console.log(`setting request price as ${amount} ${asset}`);

    const accepts = [
      solana.x402Exact({
        network: "devnet",
        asset,
        amount,
        payTo: "81CR7uzzReplaceThisWithYourWalletAddress",
      }),
      evm.x402Exact({
        network: "base-sepolia",
        asset,
        amount,
        payTo: "0xd64d67BeC49dA9ReplaceThisWithYourWalletAddress",
      }),
    ];

    return await handleMiddlewareRequest({
      facilitatorURL: "https://facilitator.corbits.dev",
      accepts,
      resource: c.req.url,
      getHeader: (key) => c.req.header(key),
      getPaymentRequiredResponse,
      sendJSONResponse: (status, body) => {
        c.status(status);
        return c.json(body);
      },
      body: async ({ settle }) => {
        // Settle will automatically verify the requirement before
        // processing the transaction.
        const settleResult = await settle();
        if (settleResult !== undefined) {
          return settleResult;
        }

        await next();
      },
    });
  };
};

const app = new Hono();

// Set up a route that varies its required price based on the provided path.
app.get("/protected", createMiddleware(), (c) => {
  return c.json({
    msg: "success",
  });
});

serve(app, (info) => {
  console.log(`Listening on http://localhost:${info.port}`);
});
To test this example, we’ll use @faremeter/rides to make a request, and ask to be charged $0.001 USDC:
import { payer } from "@faremeter/rides";

await payer.addLocalWallet(process.env.PAYER_KEYPAIR_PATH);
await payer.addLocalWallet(process.env.EVM_PRIVATE_KEY);

const req = await payer.fetch("http://localhost:3000/protected?amount=1000");

console.log("response is:", await req.json());
After running the rides example above, you should see the following: Expected output:
response is: { msg: 'success' }

Conclusion

As can be seen above, Dynamic Pricing is a powerful mechanism that can be used to tailor x402 pricing to your specific use case. Faremeter’s loose coupling and modularity allows you to take basic payment primitives, and build extremely expressive yet simple implementations with them, limited only by your imagination.