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

@siriusprotocol/x402

v0.3.2

Published

x402 payments settled privately on Sirius — siriusFetch (agent side) and siriusPaywall (resource-server side)

Readme

@siriusprotocol/x402

x402 payments settled privately on the Sirius rail.

x402 (Linux Foundation) revives HTTP 402 Payment Required: a resource server answers an unpaid request with payment requirements, the client pays, and retries with the payment in an X-PAYMENT header. x402 specifies the handshake only — where value actually moves is delegated to a facilitator.

This package is both halves of that handshake against the Sirius facilitator (crates/services-api/src/routes/x402.rs):

| entry point | side | what it does | | --- | --- | --- | | siriusFetch(url, opts) | agent | fetch, but pays a 402 and retries | | siriusPaywall(config) | resource server | 402s, settles, releases |

The scheme is sirius-private, and its payload is an ordinary signed Sirius L2 transfer — same fields, same canonical signing bytes as POST /api/tx/transfer. The amount and balances stay encrypted; the epoch is proven and verified natively on Solana.

npm install @siriusprotocol/x402@^0.2.0

Pin ^0.2.0. 0.1.0 is deprecated as of 2026-08-03. In 0.1.0 a settlement carried only the facilitator's success boolean, and false covers both a permanent refusal and a settle timeout. Only one of those means the money is safe, so a caller that read false as "failed" could retry and pay twice. settlementOutcome() in 0.2.0 recovers the third state and is biased toward "unknown". 0.1.0 also lacks the owner-authenticated nonce read, so siriusFetch could not pay at all against a rail run the correct way, with SIRIUS_PUBLIC_ACCOUNT_READ off.


Agent side — siriusFetch

Drop-in for fetch. A response that is not 402 is returned untouched; a 402 is paid once and the request replayed.

import { siriusFetch, readSettlement } from "@siriusprotocol/x402";

const res = await siriusFetch("https://api.example.com/v1/inference", {
  account: {
    privateKey: process.env.AGENT_SECRET_KEY!, // 32-byte Ed25519 seed, hex or bytes
    // index / accountId are optional — resolved from the API when omitted
  },
  apiBase: process.env.SIRIUS_API_BASE!, // e.g. https://api.siriusprotocol.xyz
  maxAmount: 50_000n,                    // refuse anything dearer, sign nothing
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ prompt: "hello" }),
});

const data = await res.json();
const receipt = readSettlement(res); // { success, transaction, network, payer }

What it does on a 402:

  1. reads the payment requirements from the body ({ accepts: [...] }, or a bare requirements object),
  2. picks the first sirius-private offer (override with select / network),
  3. resolves the payer's tree index + nonce and the recipient's tree index from the Sirius API (skipped entirely if you pass index, nonce, payToIndex),
  4. signs a Sirius transfer with the payer's key,
  5. replays the request with X-PAYMENT: base64(JSON(payload)).

Notes:

  • One payment per call. If the retry is also a 402, that response is returned rather than paid again.
  • The request is sent TWICE — once to draw the 402, once carrying the payment — so a paid endpoint cannot take a streaming body. Use a string, Uint8Array, URLSearchParams, Blob or FormData. A ReadableStream or async iterable is rejected before the first request, so this fails as a plain error rather than as a signed payment against a body the server never received. Note the unpaid first request does reach the server.
  • Nonces resolve automatically, including on a private rail. Three sources, in order: the nonce you pass; the public account read; and — when the operator has (correctly) left SIRIUS_PUBLIC_ACCOUNT_READ off — the owner-authenticated confidential read, which proves ownership of the index with the same key that signs the payment and reveals nothing to anyone who cannot already sign for it. That challenge is domain-separated (SIRIUS_OWNER_READ_V1) from the tx-signing tag, so the signature can never be replayed as a spend authorisation. Pass ownerRead: false to opt out, or nonce to keep your own stream authoritative across concurrent payments.
  • Refusals are X402Error with a code: malformed-402, no-acceptable-offer, over-max-amount, missing-nonce.

Resource-server side — siriusPaywall

import express from "express";
import { siriusPaywall } from "@siriusprotocol/x402";
// server-only import, no crypto in the bundle:
// import { siriusPaywall } from "@siriusprotocol/x402/paywall";

const paywall = siriusPaywall({
  price: "$0.02",                        // or base units: "20000" / 20000n
  payTo: process.env.SIRIUS_ACCOUNT_ID!, // your account_id (32-byte hex)
  facilitator: process.env.SIRIUS_API_BASE!,
  network: "solana-devnet",              // "solana" on mainnet
  asset: "0",
});

const app = express();
app.get("/v1/inference", paywall.middleware, (req, res) => {
  res.json({ answer: 42 }); // only runs once the payment has settled
});

Unpaid request → 402 with the requirements:

{
  "x402Version": 1,
  "accepts": [{
    "scheme": "sirius-private",
    "network": "solana-devnet",
    "maxAmountRequired": "20000",
    "payTo": "<your account_id hex>",
    "asset": "0",
    "resource": "https://api.example.com/v1/inference"
  }]
}

Request carrying X-PAYMENT → the payload is decoded, POST /api/x402/settle is called, and on success next() runs with the receipt on X-PAYMENT-RESPONSE. On failure the response is 402 again with the facilitator's reason in error.

The requirements sent to the facilitator are always the server's own — never the client's copy — so a client cannot pay itself, or pay less, and have the receipt accepted.

Framework-agnostic core

middleware is a thin binding over handle, which is just data in, data out:

const result = await paywall.handle({
  paymentHeader: request.headers.get("x-payment"),
  resource: request.url,
});

if (result.kind === "payment-required") {
  return new Response(JSON.stringify(result.body), { status: 402, headers: result.headers });
}
// result.settlement — the receipt; result.headers — X-PAYMENT-RESPONSE
return new Response(body, { headers: result.headers });

Pricing

maxAmountRequired is a u128 decimal string in base units of the paying asset (Sirius shares, asset 0).

| price | meaning | | --- | --- | | "20000", 20000n, { baseUnits: "20000" } | base units, used verbatim | | "$0.02", { usd: "0.02" } | US dollars, converted at the live share price |

The USD form reads r_global_raw from GET /api/solana/epoch and computes ceil(micro_usd * 1e9 / r_global_raw) — rounding up, so the server never under-charges. The rate is cached for 30s (rateTtlMs). Pass usdToBaseUnits to take the conversion over entirely. A bare "0.02" is rejected as ambiguous.


Settle, not verify, is proof of payment

POST /api/x402/verify is a pre-flight check, not a lock: balance and nonce are read at call time, so a concurrent spend from the same account can invalidate a payment between verify and settle. The paywall therefore settles and releases on the settle response. paywall.facilitator.verify(...) is exposed for pre-flight probes.

settlement.success is a boolean over THREE outcomes — use settlementOutcome

Since 2026-08-03 POST /api/x402/settle waits for the L2 transfer to apply before answering, so success: true means the payer was really debited. (It used to answer on mempool admission, which the sequencer can still refuse permanently — the same misreading that released $45 of USDC out of /api/withdraw/onchain.)

But success: false still covers two very different things:

| outcome | success | what it means | | --- | --- | --- | | applied | true | the payer WAS debited. Terminal. | | dropped | false | permanently refused at apply; nothing was debited. Terminal. | | timed out | false | UNKNOWN. It may still apply. NOT terminal, and NOT a failure. |

Reading a timeout as a failure is the expensive direction — the natural response to a failed payment is to send it again. So do not branch on the boolean:

import { readSettlement, settlementOutcome } from "@siriusprotocol/x402";

const receipt = readSettlement(res);
if (receipt) {
  switch (settlementOutcome(receipt)) {
    case "applied": break;                    // debited
    case "dropped": break;                    // nothing moved; safe to retry
    case "unknown": break;                    // DO NOT retry — reconcile instead
  }
}

settlementOutcome is biased toward "unknown": only a reason that positively identifies a permanent refusal is called terminal. Resolve an "unknown" with GET /api/tx/:hash/receipt at settlement.transaction, which the facilitator sets in all three cases for exactly that purpose.

Proving and Solana verification follow asynchronously after application.

Operator flags this SDK is affected by

| flag | effect | | --- | --- | | SIRIUS_PERMISSIONLESS_TX=1 | verify/settle are open; otherwise pass facilitatorToken | | SIRIUS_PUBLIC_ACCOUNT_READ=1 | nonces are readable directly. Leave it OFF (it exposes every account's balance sheet); siriusFetch then falls back to the owner-authenticated read | | SIRIUS_REAL_PRIVACY=1 | mounts the owner-authenticated read, which is what makes the nonce fallback available | | SIRIUS_REQUIRE_ACCOUNT_AUTH=1 | account reads need a bearer — pass apiToken | | SIRIUS_SOLANA_CLUSTER | decides the network label (solana-devnet / solana) and the Solana cluster every signature is bound to | | SIRIUS_CHAIN_ID | names a chain outside the solana namespace (e.g. eip155:56). Outranks SIRIUS_SOLANA_CLUSTER |

Signing

The Ed25519 public key is the account id. The signature covers "SIRIUS_L2_V1" || 0x1F || <chain id, ASCII> || 0x01 || from_index:u64 || to_index:u64 || from_account_id:32 || to_account_id:32 || amount:u128 || nonce:u64, all integers little-endian — a byte-exact port of canonical_signing_bytes in crates/services-api/src/tx_signing.rs, pinned in test/canonical.test.ts against a vector captured from the Rust function. The network you pay on is now inside the signed bytes, as a "<namespace>:<reference>" chain id — solana:<genesis hash> for Solana, eip155:<chain id> for an EVM chain. Before 2026-08-06 network rode entirely outside the signature and was only string-compared by the facilitator, so a payment signed against a devnet facilitator was byte-identical to one for mainnet; until 2026-08-07 the component was a bare Solana genesis hash, which separates Solana's clusters and not chains. Every signature minted under either older format is now rejected. siriusFetch takes the chain from the 402 offer's network, so callers need do nothing; a caller building payloads by hand passes { cluster } or { chainId } to authFor.

It is the same encoder @siriusprotocol/sdk ships, narrowed to the Transfer variant so this package stays free of @solana/web3.js; if you already hold a @siriusprotocol/sdk account you can pay over x402 with no new signing code.

Packaging

ESM + CJS, TypeScript strict, no any in the public surface. The only runtime dependencies are @noble/ed25519 and @noble/hashes, both of which the paywall half avoids — import @siriusprotocol/x402/paywall on a server that never signs.

@noble/ed25519 v2 is ESM-only, so the CJS build relies on Node's require(esm) support: Node >= 20.19 (or >= 22.12). Pure-ESM consumers have no such floor.

npm install
npm run build      # dist/esm + dist/cjs
npm test           # vitest
npx tsc --noEmit   # typecheck, incl. tests

MIT.