@craigruks/x402-server-guard
v0.1.1
Published
Server-side hardening middleware for x402 payment endpoints. Mitigations for settlement races, replay, cross-resource substitution, and cache leakage. Not a security guarantee.
Maintainers
Readme
x402-server-guard
Server-side hardening middleware for x402 payment endpoints.
Documentation: https://craigruks.github.io/x402-server-guard/
x402 lets a server charge for a resource by returning 402 Payment Required and
verifying a signed payment. Published research has shown that a naïve resource
server is exploitable in several ways: a payment can be replayed, reused against
a different resource, raced to duplicate delivery before settlement confirms, or
leaked to unpaid clients through a shared cache. This library is the enforcement
layer a merchant wraps their endpoint in to close those gaps.
[!WARNING] Status: early, pre-1.0. All four enumerated attack classes below have a mitigation implemented. It is not audited and is not a security guarantee; it mitigates these specific classes only and cannot make an insecure endpoint safe on its own. See the mitigation table for scope.
Security disclaimer
This software is provided "AS IS", without warranty of any kind. It is not audited and is not a security guarantee. It mitigates specific, enumerated attack classes only. It cannot make an insecure payment endpoint safe on its own. The authors accept no liability for any loss of funds or damages. See SECURITY.md and the LICENSE.
Install
npm install @craigruks/x402-server-guardNode ≥ 22. Zero runtime dependencies. Ships ESM with type declarations; a TypeScript or JavaScript consumer both import the same build.
Usage
Reserve a payment's nonce through the guard before you grant the resource. The
first request for a nonce wins; a replay or a concurrent race is denied. The
decision is a value, never a throw, so a stray try/catch cannot turn a deny
into an accidental grant.
import { createGuard } from "@craigruks/x402-server-guard";
const guard = createGuard();
// Inside your paid handler, after the facilitator verifies the payment:
const reservation = await guard.reserve({
nonce: authorization.nonce, // the EIP-3009 nonce
resource: request.url, // which resource this payment is for
expiresAt: Number(authorization.validBefore), // unix seconds
});
if (!reservation.reserved) {
return deny(reservation.reason.code); // e.g. "nonce-already-reserved"
}
// Settle before granting: a payment that fails to settle yields no resource.
const settled = await facilitator.settle(payload, requirements);
if (!settled.success) {
return deny("settle failed");
}
return grant(resource);[!WARNING]
createGuard()with no store uses an in-memory store that protects one process only. On Cloudflare Workers, Vercel, AWS Lambda, or any autoscaled fleet, each isolate holds its own map, so replay and race protection do not hold across instances. For those deploys, pass a store backed by an atomic compare-and-set: a Durable Object, RedisSET NX, or a database unique constraint (not a plain get-then-put store like Workers KV, which reopens the race condition). Any backend that implements theNonceStorecontract works. One is built start to finish in the box, a Cloudflare Durable Object adapter:import { createDurableObjectNonceStore } from "@craigruks/x402-server-guard/cloudflare"(see the deployment guide). The atomic compare-and-set contract for other backends is indocs/hardening.md.
One call: protect
protect runs the whole secure flow (reserve → settle → (confirm) → deliver) and
returns the cache directives on grant, releasing the reservation if the settle
fails or finality is not reached. It has no runtime dependencies and takes plain
callbacks, so it drops into any framework:
import { protect } from "@craigruks/x402-server-guard";
// After the facilitator verifies the payment:
const decision = await protect(
guard,
{ nonce, resource: request.url, expiresAt: Number(authorization.validBefore) },
{
// settle resolves true only when the payment actually settled.
settle: async () => (await facilitator.settle(payload, requirements)).success,
deliver: () => resource,
// Grant on settle success (finality rests with the facilitator and the chain).
// Use `finality: "confirm"` with a `confirm()` callback to hold for k confirmations.
finality: "facilitator",
},
);
if (!decision.granted) return deny(decision.reason.code);
response.headers.set("Cache-Control", decision.cacheControl);
return grant(decision.resource);Two runnable examples: examples/secure-flow.ts
(the concurrent race, blocked) and examples/hono-server.ts
(a Hono route protected end to end). Bind the nonce to the served route (the
request URL), not the payload's claimed resource, which is why the binding lives at
the framework layer.
All four enumerated attack classes are covered; see the table below.
Design principles
- Zero runtime dependencies. The core uses only the Web Platform
cryptoglobal (crypto.randomUUID), present on Node 22+, Cloudflare Workers, and Deno, so the guard runs in any modern runtime without a polyfill. Every dependency is attack surface; a hardening library should have as little of it as possible. Independent signature verification would need cryptographic primitives, so it stays out of the core path by design; the facilitator verifies payments, and the guard hardens the flow around that. - Installs clean under npm v12's hardened defaults: no lifecycle scripts, no
npm approve-scriptsstep, nothing to allow. - Small enough to read. Source files are capped so the whole library can be
audited in an afternoon. Built with plain
tscso the published output maps one-to-one to the source you can see. - Framework-agnostic core.
protecttakes plain callbacks, so the same guard drops into Hono, Express, Next, or Fastify. A Hono binding is shown in the examples; an@x402/core-hook convenience wrapper is a thin layer overprotect.
Taking the dependency (or not)
If you run a payment endpoint, you are right to want as few dependencies as possible. This library has none at runtime, and the core is small enough to read in one sitting. If you would rather not add a dependency at all, lift the primitives you use straight into your own code: the reservation, the finality hold, and the cache directives are each a small, self-contained file, and a coding agent can pull over the parts you need in a few minutes. That is a supported way to use this.
If you do take the dependency, we have tried to make depending on us safe. Releases publish through GitHub Actions using npm trusted publishing (OIDC), so no long-lived npm token exists for an attacker to steal; tokens are disallowed on the package, publishing requires two-factor auth, and CI-published releases carry build provenance that ties the package to the exact commit and workflow that built it.
Mitigations
Each ships with a paired test proving the attack against a vanilla server and
proving it blocked by the guard. Every class is mapped to its research, mechanism,
and proving test in docs/coverage-map.md; the rationale
is in docs/hardening.md. The hardest questions about scope
and honesty (is this a strawman, does the reference actually have these gaps, what
this does not do) are answered in
docs/objection-handling.md.
| Attack class | Status | | --- | --- | | Duplicate-settlement race | done | | Payment replay | done (same nonce reservation) | | Cross-resource substitution | done (same nonce reservation, distinct reason) | | Grant-before-finality (k-confirmations) | done | | Cache leakage of paid content | done |
Development
The local toolchain is pinned with mise:
mise install # installs Node (Active LTS) + just from mise.toml
npm ci # installs the dev toolchain, exact-pinned
just # lists every repo command
just check # full local gate: typecheck + lint + file-length + testsNode 24 (Active LTS) is used locally; the published package supports Node ≥22,
and CI tests both. npm run build emits the package with plain tsc.
Dev tooling lives in package.json scripts (npm run …); the justfile
is the discoverable index for repo operations that aren't npm: supply-chain
checks, CI, release prep. Run just to see them all.
Acknowledgments
Shengchen Ling, an author of "Free-Riding the Agentic Web: A Systematic Security Analysis of x402 Payments" (arXiv:2605.30998), reviewed how this library maps to the paper's flaw classes and invariants and confirmed it aligns with the paper's intended interpretation. This is not an audit or endorsement of the implementation.
License
MIT © Craig Ruks
