npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@vaduno/x402

v0.7.1

Published

x402 rail adapter for Vaduno. Governs HTTP 402 stablecoin payments with policy, mandates, and a tamper-evident audit trail. Never signs or holds keys to funds — you plug in your own payer.

Readme

@vaduno/x402

⚠️ Experimental — never run against a real x402 server

This adapter implements x402 v1 and v2 (the HTTP transport). It has never run against a live x402 server or facilitator. The demo and every test mock both the server and the payer in-process — the v2 suite includes the spec's own wire examples as frozen conformance vectors (spec/vectors/x402-http-v2.json) — so what is verified is that the code agrees with the specification's text and examples, not that it interoperates with anything deployed. Treat it as a reference implementation, exactly like the Stripe adapter's caveat.

Policy + audit for x402 stablecoin payments.

x402 revives HTTP 402 Payment Required: a server answers with a price, your agent pays, and retries. This package wraps that flow so every payment passes a @vaduno/guard spend firewall first, and lands in a tamper-evident audit ledger.

Vaduno never sees your keys. You supply the signer; the guard only decides whether it may run. That holds for both protocol versions — v2 adds no signing on Vaduno's side (the v2 PaymentPayload, and any sign-in-with-x identity challenge, are your payer's to build).

npm install @vaduno/x402 @vaduno/guard

Use it like fetch

import { createX402Fetch } from "@vaduno/x402";

const fetchWithPay = createX402Fetch({
  guard,                                   // your VadunoGuard
  agentId: "researcher-agent-1",
  pay: (req) => myWallet.signX402(req),    // v1 signer — keys stay yours
  v2: {
    // v2 is OPT-IN. Without this block, a v2 server (PAYMENT-REQUIRED
    // header) is refused with V2_NOT_CONFIGURED and nothing is paid.
    pay: (req, ctx) => myWallet.signX402V2(req, ctx),
  },
  assets: [                                // bind spend to the REAL token, not a label
    // v1 names and v2 CAIP-2 ids are SEPARATE keys — author both if you serve both.
    { network: "base",        asset: "0x833589...2913", symbol: "USDC", decimals: 6 },
    { network: "eip155:8453", asset: "0x833589...2913", symbol: "USDC", decimals: 6 },
  ],
});

// 402s are paid under policy; everything else passes straight through.
const res = await fetchWithPay("https://api.example.com/premium");

Which protocol version runs, exactly

Version routing is per-response, total, and single-carrier:

  • PAYMENT-REQUIRED header present on the 402 → v2. The header (base64 JSON, size-capped) is parsed; the response body is never read — so a server cannot present one price in a v1 body and another in the v2 header and have different layers read different carriers. The paid retry sends PAYMENT-SIGNATURE; settlement is read from PAYMENT-RESPONSE (including the specced failure form, 402 + {success: false}).
  • No header → v1. The JSON body is parsed exactly as before.
  • x402Version is checked totally. On the body carrier only absent (back-compat) and the integer 1 parse; 2 in a body, "2", 1.5, 0, negative, null — every one is a named refusal, never coerced. On the header carrier only the integer 2 parses. Versions above 2 raise X402VersionUnsupportedError.
  • v2 without the v2 option is refused (V2_NOT_CONFIGURED), not half-paid: a spend firewall must not silently start paying a protocol version you never configured a signer for.

Rail-specific security notes

These are the sharp edges of x402 specifically — read them before going live:

  • merchant.url is the endpoint you actually contacted, not the server's resource claim (per-requirement in v1, body-level resource.url in v2), so host allowlists bind where you really connect. A server claiming a different origin than the one reached is refused (RESOURCE_ORIGIN_MISMATCH) before pay() is called.

    This will refuse the official x402 reference endpoint, and that is not a bug. Fetching https://x402.org/protected returns a requirement whose resource.url is https://x402.vercel.app/protected — the public domain fronts a Vercel deployment, and the server reports the deployment URL. Verified live 2026-08-05: the refusal fires and pay() is never reached.

    The check stays strict by default because "the host I paid is the host I asked" is exactly the property it exists to enforce, and a client cannot tell a friendly CDN alias from a hostile redirection of funds. If you have independently satisfied yourself that an alias is legitimate, set requireResourceOriginMatch: false and pin the recipient another way — an id:<payTo> merchant pattern binds the address that actually receives the money, which is the thing you care about.

  • Money goes to payTo, decoupled from the request host — and a host allowlist is now REFUSED, not just discouraged. merchants.allow is disjunctive (an intent passes if any entry matches), so a host-form entry there can only widen the recipient constraint: ["host:api.example.com", "id:0x…"] means "this recipient or anyone that host names", and the conjunction is not expressible. Since payTo is what the authorization actually commits to, this adapter refuses a policy carrying a host-form allow entry with RECIPIENT_UNGATED before the payer runs. Measured before the gate: merchants.allow: ["host:api.example.com"] against a server on that host naming an arbitrary payTo paid, 200 OK. Pin recipients with id:<payTo> entries; put host patterns in merchants.block (where a match always denies, so disjunction only tightens); or pass allowHostOnlyMerchantPolicy: true to accept host-based allowlisting explicitly. In v2, payTo may be a role constant (e.g. "merchant") resolved out of band. Values matching ^[a-z]{1,16}$ are refused by default (PAYTO_ROLE_REFUSED) because an unresolvable recipient cannot be allowlisted; opt in per role via v2.allowPayToRoles. That is a shape heuristic, not a list of rolesMERCHANT, merchant1 and merchant_wallet do not match it and are treated as addresses. Pin the recipient with an id:<payTo> pattern if you need it constrained rather than merely sniffed.

  • intent.network carries the settlement chain — constrain it with policy.networks. Currency is not a chain: USDC on Base Sepolia and USDC on Ethereum Sepolia produce the identical intent shape and the same currency code, and before 0.6.1 the network lived only in metadata, which no policy rule reads. This adapter now sets intent.network (v1: the x402 network name, "base-sepolia"; v2: the CAIP-2 id, "eip155:84532"). For the EVM chains the x402 registry defines, policy.networks canonicalizes both spellings to one chain via a curated alias table — block: ["eip155:84532"] blocks "base-sepolia" and vice versa, so a seller cannot answer in whichever spelling your list did not name (non-EVM names are not aliased; name both spellings). The assets registry keeps v1 names and CAIP-2 ids as separate keys by design — author v2 entries explicitly. Matching is case-insensitive with no wildcards, and CAIP-2 ids are compared structurally — the id comes from an untrusted seller, so eip155:084532 is the same chain as eip155:84532 (leading zeros cannot spell around a blocklist) and a colon-bearing id that does not parse as CAIP-2 is denied (NETWORK_UNPARSEABLE), never passed through. A deployment where nothing positively constrains the chain is refused (NETWORK_UNGATED) before the payer runs: on this rail the settlement network is always known, so chain-blindness is never a deliberate configuration. Satisfy the gate with an assets registry (recommended — it also pins the token contract), a policy.networks.allow list, or the explicit allowChainBlind: true — a networks.block list alone does not satisfy it, because a blocklist admits every chain it does not name. The gate re-reads the guard's live policy, so a setPolicy() that drops the allow list re-closes payment rather than silently reopening the hole.

  • Pass the assets registry. Without it, currency comes from the server's spoofable extra.symbol. With it, a token that isn't on your list is refused. v2 entries are keyed by CAIP-2 id and matched case-sensitively (asset case-insensitive only on eip155: EVM networks); an entry for v1's "base" does not trust "eip155:8453".

  • Redirects are never followed (redirect: "manual"), on both versions. A 3xx could otherwise divert the probe — or the paid retry, leaking the X-PAYMENT / PAYMENT-SIGNATURE bearer — to another origin.

  • Spend is counted from the moment the guard authorizes it, and a failure never gives it back. Once the payment header is transmitted it is a bearer authorization the server can still settle while returning an error — but the accounting is stricter than that: ANY thrown executor keeps the spend counted, including a signer that failed before sending and a transport that died mid-request, because a throw cannot distinguish "never sent" from "sent, then the connection died". Over-hold, never overspend. If you can PROVE the rail did not charge, guard.releaseSpend(intentId) reclaims it explicitly; this adapter never calls it for you. Under v2's upto scheme the counted amount is the authorized maximum (what your signature permits); the smaller settled amount an untrusted PAYMENT-RESPONSE may later report is never reconciled downward. Bind a consume-once mandate (maxUses) to bound retries.

  • The v2 schemes this adapter was built against carry no reusable authorizations. exact is single-use by EIP-3009 nonce and upto settles at most once, so per-authorization counting matches them exactly; sign-in-with-x re-access is a server-side identity grace where no payment occurs. A batch-settlement scheme also exists in the spec tree — a signed running total redeemed at session end — and was not analysed for this work. Counting stays conservative under it rather than complete: each transmitted signature is counted at its stated amount, so a batch would be over-counted, never under. Read this as the schemes examined, not as a claim about every scheme the spec may define.

  • Untrusted input is bounded on every path. v1: the 402 body is byte-capped (64 KB, counted as received, abort at the cap). v2: the PAYMENT-REQUIRED header is size-capped before decoding, extensions (which your payer must echo) are structurally validated, size-capped, and defensively copied, and the 402 body is not read at all. accepts is capped on both. extra is validated by key and type and defensively copied on both.

  • What is validated is what is paid. The requirement handed to your pay() / v2.pay() is the same fixed-allowlist object the guard policed — unknown keys from the wire are dropped, and v2 requirements carrying v1 money/binding fields (maxAmountRequired, per-requirement resource) are refused as mixed-version shapes.

Errors

| Thrown | Meaning | |---|---| | X402ProtocolError | The 402 response was malformed, over a size cap, wrong-typed, or version-confused — no money moved | | X402VersionUnsupportedError | The server declared a version this adapter refuses on that carrier (v3+, or v2 declared inside a JSON body), or sent v2-SHAPED data with no version declared at all — the undeclared arm is what turns a misleading field error into a named version error — no money moved | | X402RequirementRefusedError | Refused before paying (V2_NOT_CONFIGURED, PAYTO_ROLE_REFUSED, ASSET_NOT_ALLOWED, RESOURCE_ORIGIN_MISMATCH, NO_REQUIREMENT_SELECTED, NETWORK_UNGATED, RECIPIENT_UNGATED) — no money moved | | X402PaymentBlockedError | Policy / mandate / freeze blocked it — no money moved | | X402PaymentFailedError | Payer ran; check .transmitted — if true, spend is counted because the server may still settle |

Security

Read SECURITY.md for the full threat model and known limitations.

License

MIT