Skip to main content

Quickstart: Sellers

Put a price on an HTTP route and let HPP's facilitator handle verification and onchain settlement. This example uses the @x402/express middleware, but the same pieces apply to any HTTP framework the SDK supports.

1. Install

npm install @x402/express @x402/core @x402/evm @x402/extensions express

2. Register USDC.e

HPP's USDC.e is registered in the SDK's default-asset map so the exact (EIP-3009) domain (Bridged USDC / version 2 / 6 decimals) is filled in automatically. Run this once at startup, before constructing the resource server:

import { DEFAULT_STABLECOINS } from "@x402/evm";

const USDC_E = {
address: "0x401eCb1D350407f13ba348573E5630B83638E30D",
name: "Bridged USDC",
version: "2",
decimals: 6,
};
DEFAULT_STABLECOINS["eip155:190415"] = USDC_E; // HPP Mainnet
DEFAULT_STABLECOINS["eip155:181228"] = USDC_E; // HPP Sepolia

3. Build the resource server

Point a facilitator client at HPP's hosted facilitator and register the schemes you want to accept on each network.

import { x402ResourceServer } from "@x402/express";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { UptoEvmScheme } from "@x402/evm/upto/server";
import { HTTPFacilitatorClient } from "@x402/core/server";

const facilitator = new HTTPFacilitatorClient({ url: "https://facilitator.hpp.io" });

const resourceServer = new x402ResourceServer([facilitator]);
resourceServer.register("eip155:190415", new ExactEvmScheme());
resourceServer.register("eip155:190415", new UptoEvmScheme());

Only register what the facilitator supports. The resource server throws if you advertise a (network, scheme) pair that the facilitator does not return from /supported. exact and upto are supported on both HPP networks. You can pass multiple facilitator clients to x402ResourceServer([...]) to serve more than one network from a single process.

4. Price your routes and mount the middleware

Each route lists one or more accepts (one per (network, scheme)), priced in USDC.e base units.

import express from "express";
import { paymentMiddleware } from "@x402/express";
import type { RoutesConfig } from "@x402/core/server";

const PAY_TO = "0xYourReceivingAddress"; // where settled USDC.e lands

const routes: RoutesConfig = {
"POST /paid/hello": {
description: "A paid hello-world endpoint.",
accepts: [
{
scheme: "exact",
network: "eip155:190415",
payTo: PAY_TO,
price: {
amount: "10000", // 0.01 USDC.e (6 decimals)
asset: "0x401eCb1D350407f13ba348573E5630B83638E30D",
extra: { name: "Bridged USDC", version: "2" },
},
maxTimeoutSeconds: 600,
},
],
},
};

const app = express();
app.use(paymentMiddleware(routes, resourceServer));

app.post("/paid/hello", (_req, res) => {
res.json({ message: "Thanks for paying — here is your resource." });
});

app.listen(4021, () => console.log("x402 resource server on :4021"));

That's it. An unpaid POST /paid/hello now returns 402 with your terms; a request carrying a valid payment-signature header is verified, served, and settled onchain — and the handler returns the resource.

Accepting upto and gasless payments

To also accept the upto scheme, list it in the route's accepts (in priority order — the buyer's SDK picks the first it supports) and register it on the network alongside exact.

For gasless upto — letting a buyer with zero native ETH pay — the route must declare the EIP-2612 gas-sponsoring extension. Without it, the facilitator cannot sponsor the buyer's Permit2 approval, and a buyer without a standing allowance is rejected with permit2_allowance_required.

import { declareEip2612GasSponsoringExtension } from "@x402/extensions";

// Replaces the route from step 4. `PAY_TO` and the `price` object are the same
// as before (here `price` is the { amount, asset, extra } object shown above).
const routes: RoutesConfig = {
"POST /paid/hello": {
description: "A paid hello-world endpoint.",
accepts: [
{ scheme: "exact", network: "eip155:190415", payTo: PAY_TO, price, maxTimeoutSeconds: 600 },
{ scheme: "upto", network: "eip155:190415", payTo: PAY_TO, price, maxTimeoutSeconds: 600 },
],
// Required for gasless `upto`: lets the facilitator sponsor the buyer's
// one-time Permit2 approval via EIP-2612 (so a 0-ETH wallet can pay).
extensions: { ...declareEip2612GasSponsoringExtension() },
},
};

The facilitator must advertise eip2612GasSponsoring for the network (HPP Mainnet and Sepolia both do — see Facilitator → Gasless settlement).

Settling the actual amount (metered billing)

Advertising upto (above) only sets the maximum. The point of upto is that you settle only what was actually used, computed after the work runs — which is what makes it the right scheme for usage-based pricing: LLM token counts, compute time, bytes served. The advertised price is the cap; the final charge is always ≤ the authorized max, and settling 0 costs no onchain transaction at all.

After your handler runs, tell the middleware how much to actually charge with setSettlementOverrides (exported from @x402/express) — it settles that amount instead of the advertised max:

import { setSettlementOverrides } from "@x402/express";

// A metered route: charge for what the model actually used.
app.post("/paid/llm", async (req, res) => {
// The `upto` payment is already verified by the middleware;
// settlement runs AFTER this handler returns.
const result = await runModel(req.body); // do the metered work

// Your usage → USDC.e atomic units, as a STRING, always ≤ the advertised max.
const chargeAtomic = usageToUsdcE(result.usage); // e.g. "1000" = 0.001 USDC.e

// Settle the actual amount instead of the max. Pass "0" and nothing settles onchain.
setSettlementOverrides(res, { amount: chargeAtomic });

res.json(result); // 200 → the facilitator settles `chargeAtomic` (≤ max) onchain
});

amount is a string of atomic USDC.e units. The SDK also accepts percentage and dollar-price override forms, and there's more to the scheme semantics — for those we point at the upstream docs rather than duplicate them (they track the SDK, which changes often):

What's HPP-specific: both HPP facilitators support upto with variable settlement (settled ≤ max) on HPP Mainnet (eip155:190415) and HPP Sepolia (eip155:181228) — you can confirm it live at GET https://facilitator.hpp.io/supported (and …-sepolia). Once the SDK is pointed at the HPP facilitator + USDC.e (steps 2–3), the upstream upto recipe settles onchain on HPP unchanged.

Gateway / non-Node sellers

Building the seller on an API gateway (Kong/Lua) or another language? The metering lives in whatever layer counts usage. Two paths:

  • Sidecar (recommended) — run the @x402 resource-server middleware as a Node sidecar behind your gateway and reuse its upto settlement as-is (nothing to reimplement).
  • Native — call the facilitator's /verify and /settle HTTP endpoints directly from your language. The wire contract is the standard x402 protocol; see Facilitator.

Reacting to settlement

res.json() returns right away, but the middleware sends the response only after it settles (and only on success — status < 400). Your handler can't see the settle result inline. To run logic after settlement — record usage, credit an account, emit a webhook — register hooks on the resource server:

resourceServer.onAfterSettle(async (ctx) => {
// ctx.result carries the settlement outcome (success, onchain tx, payer).
// e.g. record the charge or emit a webhook.
});

resourceServer.onSettleFailure(async (ctx) => {
// The work ran but settlement failed — the buyer got an error, not the resource.
// e.g. log it for reconciliation.
});

The full hook set is in the upstream lifecycle hooks reference.

Next