@axiru/x402-policy-middleware
v0.1.1
Published
Policy pre-authorization middleware for x402 (HTTP 402 Payment Required) flows. Wraps an x402 facilitator request with a governance decision check and emits a signed authorization token the signer can verify before broadcasting on-chain.
Downloads
78
Maintainers
Readme
@axiru/x402-policy-middleware
Policy pre-authorization for x402 (HTTP 402 Payment Required) flows. Wraps an x402 facilitator request with a governance decision check and emits a signed authorization token that the signer verifies before it broadcasts on-chain.
Apache-2.0. Transport agnostic. No I/O of its own.
Why
x402 flows skip your fraud, compliance, and approval gates by design. The agent makes an HTTP call, the resource server replies 402 Payment Required, the agent signs an on-chain transfer, the facilitator broadcasts. There is no merchant of record between the agent and on-chain settlement, and nothing in the protocol asks whether your organization wanted that payment to happen.
This package sits between the agent and the signer and re-introduces that question:
┌─────────┐ 402 Payment Required ┌─────────────┐
│ Agent │ ─────────────────────► │ Facilitator │
└────┬────┘ └─────────────┘
│
│ authorizeX402Request(challenge, context, deps)
│
▼
┌──────────────────────────┐
│ @axiru/x402-policy- │── callDecisionEngine ──► your policy decision endpoint
│ middleware │
│ │── signAuthorizationToken ──► your signer
└──────────────────────────┘
│
│ MiddlewareResult { status: "allowed" | "blocked" | "internal_error" }
▼
Signer verifies the authorization token, then broadcasts
(or fail-closed refuses to broadcast).The middleware is transport agnostic, I/O free, and pure beyond its injected dependencies. It runs on Node, Bun, Deno, Cloudflare Workers, or in a Lambda.
Install
npm install @axiru/x402-policy-middlewareThe only runtime dependency is @axiru/spec, the shared policy and evidence vocabulary.
Quickstart
import {
authorizeX402Request,
toHttpResponse,
type X402PaymentRequirements,
type X402RequestContext,
type MiddlewareDependencies,
} from "@axiru/x402-policy-middleware";
const deps: MiddlewareDependencies = {
callDecisionEngine: async (ovt, policyInputs) =>
fetch("https://policy.example.com/v1/decisions", {
method: "POST",
body: JSON.stringify({ ovt, policyInputs }),
}).then((r) => r.json()),
signAuthorizationToken: async (claims) =>
fetch("https://policy.example.com/v1/authorizations", {
method: "POST",
body: JSON.stringify(claims),
}).then((r) => r.text()),
fingerprintOVT: (ovt) => sha256(canonicalJson(ovt)),
issuer: "https://policy.example.com",
};
// Inside your agent's x402 handler:
const result = await authorizeX402Request(challenge, context, deps);
if (result.status === "allowed") {
// Retry the facilitator with the authorization token attached:
await fetch(challenge.resource_url, {
headers: { "axiru-authorization": result.authorization_token },
});
} else {
// Fail-closed: do NOT broadcast.
const http = toHttpResponse(result);
return res.status(http.status).json(http.body);
}fingerprintOVT must be a stable canonical-JSON SHA-256 over the transfer. Any implementation works as long as it is deterministic across processes; the fingerprint is the replay key that ties a decision to the exact transfer it authorized.
Fail-closed contract
The middleware never returns allowed unless both of the following hold:
- The decision endpoint explicitly returned
allow, and - The signer returned a non-empty token string.
Every other outcome produces a non-allowed result: the decision call throws, the signer throws, the signer returns empty, the payload is structurally invalid, or the decision is deny, quarantine, or require_approval. The signer is contractually required to refuse to broadcast on any non-allowed result.
HTTP status mapping
toHttpResponse maps results to HTTP wire codes:
| Result | Status | Notes |
| ------------------------------ | -----: | -------------------------------------------------- |
| allowed | 200 | axiru-authorization header carries the JWS token |
| blocked (deny) | 403 | Governance refused |
| blocked (quarantine) | 423 | Locked, released by a human |
| blocked (require_approval) | 428 | Precondition Required, approval queue |
| internal_error (anything) | 503 | Fail-closed; signer must refuse |
Authorization-token claims
interface AuthorizationTokenClaims {
jti: string; // recorded against the decision record
iss: string; // your issuer URL
sub: string; // org_id
aud: string; // facilitator_url
iat: number; // Unix seconds
exp: number; // Unix seconds (default iat+120)
event_id: string; // decision event id
ovt_fingerprint: string; // audit replay key
rail: "x402";
rail_action: "pay";
pay_to_address: string;
amount_minor_units: string; // serialized bigint
asset: string;
chain: string;
reason_code: string;
}The host signer JWS-signs these claims. The middleware itself never touches key material.
MPP charge intents
The package also maps Merchant Payment Protocol charge intents onto the same transfer shape, so one policy set covers both x402 and MPP without a second rule language. See mpp-charge-intent.ts and its tests for the mapping.
Related packages
@axiru/specis the policy and evidence vocabulary.@axiru/x402-receipt-verifierverifies the signed offers and receipts that come back after settlement.@axiru/agent-spend-guardrailsis an in-process policy evaluator you can pointcallDecisionEngineat when you do not want a network hop.
License
Apache-2.0. Copyright 2026 Axiru. See LICENSE.
