Skip to content

Payment Middleware ​

Use Alpha payment middleware when your application either protects an HTTP resource with a payment requirement or needs a provider client inside a route. One createAlphaPayment() runtime owns one provider and one direction and can be reused across requests.

The initial capability matrix is intentionally explicit:

Providerinboundoutbound
x402SupportedSupported
alipaySupportedSupported
weixinConfiguration errorSupported

Alpha delegates x402 verification, settlement, facilitator communication, and wire headers to the official @x402/express, @x402/hono, and @x402/next packages. It does not implement a second x402 protocol stack.

Runtime Requirements ​

All middleware runs on Node.js >=20.19.0. Keep private keys, facilitator credentials, Alipay gateway credentials, and WeiXin signing credentials on the server. For Next.js, explicitly select the Node runtime:

ts
export const runtime = "nodejs";

Alipay inbound needs merchant gateway credentials. Alipay outbound instead requires a host-provided payer and does not need merchant appId or RSA keys.

Install only the framework peer used by the application:

sh
pnpm add @averyso/alpha express
pnpm add @averyso/alpha hono
pnpm add @averyso/alpha next

The root @averyso/alpha entry is framework-independent. Framework code is available from @averyso/alpha/express, @averyso/alpha/hono, and @averyso/alpha/next.

Create an x402 Inbound Runtime ​

An inbound x402 runtime needs protected routes and either a preconfigured x402ResourceServer or an explicit facilitator plus scheme registration. There is no implicit facilitator URL.

ts
import { createAlphaPayment } from "@averyso/alpha";

export const payment = createAlphaPayment({
  provider: "x402",
  direction: "inbound",
  facilitator: {
    url: process.env.X402_FACILITATOR_URL!,
  },
  schemes: "auto",
  network: ["base-sepolia"],
  routes: {
    "GET /api/report": {
      accepts: {
        scheme: "exact",
        network: "Base Sepolia",
        price: "$0.01",
        payTo: process.env.X402_PAY_TO!,
      },
      description: "Generate one report",
      mimeType: "application/json",
    },
  },
});

schemes: "auto" registers the official EVM and Solana exact server schemes needed by the route networks. Route network aliases are normalized through the same registry as X402Client. The optional top-level network is only an allowlist; it never replaces a route's accepts[].network.

For custom hooks, multiple facilitators, or custom scheme behavior, construct the official server yourself:

ts
import { x402ResourceServer } from "@x402/core/server";
import { ExactEvmScheme } from "@x402/evm/exact/server";

const server = new x402ResourceServer([primaryFacilitator, fallbackFacilitator])
  .register("eip155:84532", new ExactEvmScheme())
  .onAfterSettle(auditSettlement);

export const payment = createAlphaPayment({
  provider: "x402",
  direction: "inbound",
  routes,
  server,
});

Do not combine server with facilitator or schemes. The factory rejects the conflict before serving traffic. payment.initialize() returns one cached promise, so applications may call it eagerly during startup while adapters can safely call it again.

Express ​

Use alphaExpressMiddleware() with ordinary Express handlers for x402 inbound or any outbound context:

ts
import express from "express";
import { alphaExpressMiddleware, getAlphaPaymentContext } from "@averyso/alpha/express";
import { payment } from "./payment.js";

const app = express();

app.use(alphaExpressMiddleware(payment));

app.get("/api/report", (req, res) => {
  const context = getAlphaPaymentContext(req);
  res.json({ provider: context.provider, report: buildReport() });
});

Use withAlphaExpress() when the handler should receive a Web Request and return a Web Response. This wrapper is mandatory for Alipay inbound because it buffers the response until fulfillment is confirmed:

ts
import { withAlphaExpress } from "@averyso/alpha/express";

app.get(
  "/api/report",
  withAlphaExpress(payment, async (request, context) => {
    return Response.json({
      paid: context.provider === "alipay" && context.payment !== null,
      report: await buildReport(request.signal),
    });
  }),
);

Call body parsers such as express.json() before the wrapper when handlers expect parsed request bodies. If no parser consumed the body, Alpha bridges the Node request stream to the Web Request.

Hono ​

alphaHonoMiddleware() stores the initialized outbound client or inbound context in Hono's context and delegates x402 protection to the official Hono adapter:

ts
import { Hono } from "hono";
import { alphaHonoMiddleware, getAlphaPaymentContext } from "@averyso/alpha/hono";
import { payment } from "./payment.js";

const app = new Hono();

app.use(alphaHonoMiddleware(payment));
app.get("/api/report", (c) => {
  const context = getAlphaPaymentContext(c);
  return c.json({ provider: context.provider, report: buildReport() });
});

Use withAlphaHono() for Alipay inbound or a uniform Web handler:

ts
import { withAlphaHono } from "@averyso/alpha/hono";

app.get(
  "/api/report",
  withAlphaHono(payment, async (_request, context) =>
    Response.json({ provider: context.provider, report: await buildReport() }),
  ),
);

Next.js App Router ​

withAlphaNext() wraps App Router route handlers. Alpha passes payment context as the second callback argument rather than modifying NextRequest. The normal Next route context is available as the third argument.

ts
// app/api/report/route.ts
import { withAlphaNext } from "@averyso/alpha/next";
import { payment } from "@/server/payment";

export const runtime = "nodejs";

export const GET = withAlphaNext(payment, async (_request, context) => {
  return Response.json({ provider: context.provider, report: await buildReport() });
});

Protect pages or wildcard paths from proxy.ts with alphaNextProxy(). This API accepts only an x402 inbound runtime:

ts
// proxy.ts
import { alphaNextProxy } from "@averyso/alpha/next";
import { payment } from "@/server/payment";

export const proxy = alphaNextProxy(payment);

export const config = {
  matcher: ["/reports/:path*"],
};

Do not put Alipay or WeiXin logic in an Edge-only proxy. Pages Router adapters are not part of the initial release.

Outbound Contexts ​

Outbound middleware injects a configured client; it never patches global fetch.

ts
const x402Outbound = createAlphaPayment({
  provider: "x402",
  direction: "outbound",
  network: "base-sepolia",
  privateKey: process.env.X402_PRIVATE_KEY!,
  rpcUrl: process.env.X402_RPC_URL,
  maxAmount: 50_000n,
});

const weixinOutbound = createAlphaPayment({
  provider: "weixin",
  direction: "outbound",
  client: {
    developerId: process.env.WEIXIN_AI_DEVELOPER_ID!,
    publicKeyId: process.env.WEIXIN_AI_PUBLIC_KEY_ID!,
    privateKey: process.env.WEIXIN_AI_SM2_PRIVATE_KEY!,
  },
});

const alipayOutbound = createAlphaPayment({
  provider: "alipay",
  direction: "outbound",
  client: {
    payer: {
      createPaymentProof: ({ paymentNeeded, request, signal }) =>
        buyerPaymentPolicy.authorize({ paymentNeeded, request, signal }),
    },
  },
});

An x402 outbound context exposes context.client.call(). A WeiXin outbound context exposes context.client.preorder(). An Alipay outbound context exposes context.client.fetch(), which accepts standard Fetch API input and returns a raw Response. Context objects do not expose raw private keys, generated signatures, payment headers, or raw gateway responses.

Alipay Machine Pay Outbound ​

AlipayAIPayMachinePayClient handles one buyer-side challenge/retry sequence:

  1. Send the original request.
  2. Return any non-402 response, a 402 without a non-empty Payment-Needed, or a 402 for a request that already has Payment-Proof.
  3. For one eligible 402, pass the unmodified challenge and effective request URL, method, and abort signal to the injected payer.
  4. Retry once with the payer's non-empty Payment-Proof; return the second response even when it is another 402.

The client does not parse a bill, generate a proof from merchant credentials, or run an Alipay CLI. Payer implementations must enforce trusted-merchant, amount-limit, and approval policy. Request construction, payer, and replay failures throw AlipayAIPayRequestError; payment header values are excluded from diagnostics.

For a complete merchant and buyer walkthrough, see Build Alipay AI Pay with Avery SDK.

Alipay Inbound and Replay Protection ​

Configure a static bill or an async bill factory per METHOD path route:

ts
const alipayInbound = createAlphaPayment({
  provider: "alipay",
  direction: "inbound",
  client: {
    appId: process.env.ALIPAY_APP_ID!,
    privateKey: process.env.ALIPAY_APP_PRIVATE_KEY!,
    alipayPublicKey: process.env.ALIPAY_PUBLIC_KEY,
  },
  replayStore,
  routes: {
    "GET /api/report": {
      bill: async ({ request }) => ({
        outTradeNo: await reserveOrder(request),
        amount: "0.01",
        resourceId: "/api/report",
        payBefore: new Date(Date.now() + 5 * 60_000).toISOString(),
        sellerId: process.env.ALIPAY_SELLER_ID!,
        sellerName: "Example Seller",
        goodsName: "AI report",
        serviceId: "report-v1",
      }),
      maxResponseBytes: 1024 * 1024,
    },
  },
});

The flow is ordered as follows:

  1. Missing or invalid Payment-Proof receives a new signed 402 challenge.
  2. Verification must match the current bill's amount, order, and resource.
  3. The replay store atomically claims tradeNo + route.
  4. The application handler returns a complete Web Response.
  5. Alpha buffers and size-checks the response.
  6. Alipay fulfillment is confirmed.
  7. The replay claim is completed, then the response is released.

The default response limit is 1 MiB. Streaming, SSE, and already-consumed responses are rejected. If the handler fails before confirmation, Alpha calls replayStore.abandon(). If confirmation fails or enters an uncertain state, Alpha does not release the claim automatically and does not send the resource body.

Alpha permits an Alipay runtime without a replay store for development and logs one warning. Production deployments should use Redis, a database, or another durable store with an atomic claim operation. An in-process map is not provided because it is unsafe across workers and restarts.

Operational Boundaries ​

  • Scope each runtime to one provider and direction.
  • Use separate route scopes when an application accepts multiple payment rails.
  • Keep Payment-Proof, Payment-Needed, PAYMENT-*, private keys, signatures, and raw gateway responses out of logs.
  • Alpha middleware logs only provider, direction, route, status, latency, error type, and redacted network family.
  • Treat fulfillment timeouts as uncertain payment state and reconcile them before retrying.

See the Middleware API reference for the complete public types and validation rules.