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

@tollstile/express

v0.1.2

Published

Express middleware to charge per API call with x402, MPP, credits, and subscriptions. Holds the response until payment settles.

Readme

@tollstile/express

Tollstile for Express 5.

paid(gate, handler) wraps a route handler. Unpaid requests get a 402 with every rail's challenge; paid requests run your handler, and the response is held at the moment it would send its headers until the payment is completed — settled, or released. Receipt headers are on the response, and settlement has finished, before anything reaches the client.

Install

npm install tollstile @tollstile/express express

Example

import express from 'express';
import { createTollstile, memoryLedger, testRail } from 'tollstile';
import { paid } from '@tollstile/express';

const toll = createTollstile({ rails: [testRail()], ledger: memoryLedger() });
const app = express();

app.get('/weather', paid(toll.price('$0.01'), (req, res, { payment }) => {
  res.json({ forecast: 'clear', paidWith: payment.via });
}));

app.listen(3000);
curl -i localhost:3000/weather                      # 402 Payment Required
curl -i -H "Payment: test" localhost:3000/weather   # 200 OK, Payment-Receipt: test_settlement_…

Behavior

The handler is called as handler(req, res, { payment, next }).

| What happens | Payment | |---|---| | The response sends its headers with status below 400 | completed as succeeded: settled (authorization flow), receipt headers added | | The response sends its headers with status 400 or above | completed as failed: released, or refunded on the upfront flow | | The handler throws or rejects | completed as failed, then Express receives the error | | The handler calls next(error) | completed as failed when the error response is sent | | The handler calls next() | decided by whichever handler sends the response |

  • When the outcome is decided. Express responses are written synchronously, but settlement is not. The first call that would send headers — res.send, res.json, res.end, res.writeHead, res.write, res.flushHeaders, or a piped stream — decides the outcome from the status code, and that call and everything after it are held until the payment is completed. A streaming handler therefore gets its first bytes out only after settlement, and an error thrown halfway through a stream does not undo the charge: the headers, and the receipt, are already on their way.
  • When completion fails (for example, the ledger is unreachable), the held response is discarded and the error goes to your Express error handlers, which render it instead. If control had already left the handler through next() or a thrown error, the connection is closed instead of passing the error on a second time.
  • Writes made while the response is held return false; 'drain' is emitted once they are let through, so piped streams and writers that respect back-pressure resume.
  • Call payment.fulfill() inside the handler to mark the service as delivered earlier; a later failure then does not undo the charge.
  • The resource is "<METHOD> <route path>" — e.g. GET /api/users/:id — when the handler is on a string route path, and "<METHOD> <pathname>" otherwise (for example under app.use). Router mount paths are taken from req.baseUrl, so a mount path with parameters is recorded with its values; set toll.price(amount, { resource }) there.
  • Rails read proofs from a Web Request built from req: method, the absolute URL from req.protocol, req.host, and req.originalUrl (both honor Express's trust proxy setting), and every header. The body is rebuilt from what Express parsed (express.json(), express.text(), express.raw(); objects are serialized with sorted keys), so dynamic prices can read it and quotes bind to it. If a request has a body that no parser read, any price or quote commitment that reads the body fails with CONFIG_INVALID instead of seeing an empty body. Fixed-price routes never read it.

Options

paid(gate, handler, options?)

| Option | Type | Description | |---|---|---| | principal | (req: express.Request) => Principal \| null \| Promise<Principal \| null> | Resolves the authenticated caller for access policies such as subscriber() and credits(). Defaults to no principal. |

Verification status

Tested with Vitest against a real Express 5.2 app on Node's HTTP server, an ephemeral port, and fetch, using testRail(), memoryLedger(), and memoryBalance(): the 402 → pay with the echoed quote → 200 round trip; res.json, res.send, explicit res.writeHead, a piped stream larger than the socket buffer, and writers waiting for 'drain'; completion finishing before the client sees the response (with a deliberately slow complete); releases on thrown errors, rejected promises, next(error), and 4xx responses; next() to a later handler; a failing completion replacing the response; route-path resource names; a single complete() per request; and principals reaching credits().

Not tested with other middleware that patches the response, such as compression, or behind a reverse proxy. To verify such a setup, run the example with your middleware stack and run the two curl commands: the second must return 200 with a payment-receipt header and the full body, and your ledger must show the charge settled before the response was received.