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

goldrail

v0.0.2

Published

Price any HTTP endpoint in any payment dialect — the goldrail SDK and CLI.

Downloads

25

Readme

goldrail

Payments for Node, Bun, Deno and Workers. Put a price on a route, or pay for one — both sides of HTTP 402, in one package.

One word, no scope. The package is goldrail; framework shapes are subpaths of it — goldrail/express, goldrail/hono, goldrail/next — and the payer half is goldrail/pay. There is nothing scoped and nothing hyphenated to type, in this ecosystem or any other.

The seller side embeds the Rust engine as a native addon: a decision made in your Express app and one made by goldrail serve are made by the same code against the same store. No money logic is reimplemented in TypeScript, because a second implementation would be correct until the first divergence and silently wrong afterwards.

npm install goldrail

Seller: price a route

import { goldrail } from 'goldrail/express';
app.use(goldrail());

That is the integration. Prices, rails, exemptions and fail postures live in the store, not in your code — which is what keeps prices live-tunable and this snippet at one line forever. Point the engine at a store and a config:

export GOLDRAIL_STORE_URL=sqlite://goldrail.db
export GOLDRAIL_PAY_TO=eip3009=0xYourTreasuryAddress

or pass them inline for the thirty-second demo:

app.use(goldrail({ store: 'sqlite://goldrail.db', config: 'goldrail.toml' }));

Zero-arg discovery order: GOLDRAIL_* environment variables, then a goldrail.toml beside the process. If nothing names a store, construction throws and lists the options — including dev: true, which accepts an in-memory store and says so in a warning. A payment layer that could not ask the question will not answer it by serving everything for free.

Hono, Bun, Deno, Workers and Next route handlers use the fetch shape, and each has a subpath that names it:

import { goldrail } from 'goldrail/hono';        // app.use(goldrail())
import { goldrail } from 'goldrail/next';        // export const GET = goldrail(handler)
import { goldrailFetch } from 'goldrail';        // anything else with Request/Response

goldrail/next also exports middleware() for middleware.ts. Prefer the route-handler wrapper: Next's middleware runs before routing and never sees the route's response, so a request priced there is charged on the decision alone and a later 500 is not refunded. The subpath's docs say so at the export.

What the middleware does

  1. Asks the engine, forwarding only payment-related headers — never your customers' cookies or API keys.
  2. Applies the answer: serve (relay, plus goldrail-funding and goldrail-balance headers), challenge (402 carrying every enabled dialect), or reject (an RFC 9457 problem document).
  3. Reports what your handler actually returned — on a normal finish, on a thrown handler, and on a dropped connection. This is not optional: refunds on upstream failure hang off it, and a middleware that skipped it would charge payers for responses they never received.

When the engine cannot be reached

Distinct from a route's fail posture, which is about payments that fail. This is about not being able to ask at all:

| onUnavailable | Behaviour | | --- | --- | | 'closed' (default) | Answer 503. Nothing is given away, and a broken decision plane stays loud. | | 'open' | Serve unpaid. An outage in your payment plane never becomes an outage for your customers; the cost is that the endpoint is free while it lasts. |

Both are reported through onError. Neither is silent.

Admin panel, mounted in your app

import * as goldrail from 'goldrail';
app.use('/ops/goldrail', goldrail.admin());

The mounted admin carries its own authentication, RBAC and audit trail — on the Rust side. This handler forwards a method, a path and a body, and renders whatever comes back; it authenticates nothing and knows no routes, so there is exactly one implementation of the authorization boundary rather than two that drift.

An embedded engine has no admin surface unless you mount one, and mounting one requires a companion process to serve it:

goldrail admin --store sqlite://goldrail.db   # same store as your app
export GOLDRAIL_ADMIN_URL=http://127.0.0.1:8402

Without it, the admin route answers 503 with that instruction. Goldrail never ships an unauthenticated admin, mounted or not, so there is no local fallback.

Payer: pay for a route

const fetch = paidFetch(wallet, { maxPerRequest: 5000n });

fetch, with payment. It discovers the challenge, picks an option, signs, and retries once — refusing to loop when a seller re-challenges for an amount already paid, because retrying that is a spend loop wearing a retry's clothes.

import { EvmWallet, paidFetch } from 'goldrail/pay';

const wallet = EvmWallet.fromPrivateKey(process.env.PAYER_KEY!);
const pay = paidFetch(wallet, {
  maxPerRequest: 5_000n,          // never authorize more than this at once
  budget: 1_000_000n,             // lifetime ceiling across every request
  networks: ['base'],             // only pay on these chains
  payTo: ['0xSellerTreasury'],    // only pay these addresses
});

const response = await pay('https://api.example.com/v1/data');
console.log(pay.spent, pay.balance);

Every guard runs before the signature exists. An authorization that exists is collectible by whoever holds it, so checking a budget afterwards is not a budget. The lifetime ceiling is reserved rather than merely checked, which is what makes it hold for an agent with several requests in flight.

The payer is EVM-only, deliberately

paidFetch signs EIP-3009 on EVM chains (Base, Ethereum, Optimism, Arbitrum, Polygon, Avalanche, and any eip155:<id>). A Solana or Stellar challenge is refused by name with an UnsupportedMethodError — those methods need ed25519 and their own transaction encodings, and half-signing them would produce payloads that fail at the facilitator, which is a worse failure than a clear refusal here. Sellers offering several methods work fine: the selector skips the options this client cannot sign.

Use goldrail pay or your own signer for the others.

Keys stay in your process

The signer is local and dependency-free: Keccak-256 and secp256k1 with RFC 6979 deterministic k, validated in CI against viem's published signatures. The private key lives in a real private field and is never logged or serialized. Nothing here talks to a key service, and nothing here is a wallet.

Which transport am I on?

The native addon is the default. If a prebuilt binary is unavailable for your platform, the package falls back to the decision API of a running goldrail process (GOLDRAIL_URL) and prints one warning line saying so — decisions become a network hop and report becomes best-effort, which is a fine trade to make deliberately and a bad one to make by accident.

const middleware = goldrail();
const engine = await middleware.ready;
console.log(engine.transport); // 'native' | 'decision-api'

Set transport: 'native' to turn a missing addon into a startup error instead of a fallback — which is usually what a production deployment wants.

Headers, which are contract surface

| Header | Meaning | | --- | --- | | goldrail-funding | settled · credit · pending · exempt · shadow · free · unpaid | | goldrail-balance | Atomic units remaining after this request | | goldrail-amount-required | On a 402: the amount that converges (a top-up on the credit rail, the price on the per-request rail) | | goldrail-ignored-directives | Directives the route did not allowlist — ignored, and said so |

These names are semver-governed: assert on them in CI and minor upgrades will not break you.

Building the addon from source

cd sdk/typescript
npm install
npm run build:native      # napi build → native/goldrail.<platform>-<arch>.node
npm test

No build artifacts are committed: CI builds one addon per platform and publishes them as the optional dependencies the loader resolves. Those per-platform packages are the only scoped names in the project and nobody ever types onenpm i goldrail pulls the right one for your machine.

GOLDRAIL_NATIVE=/path/to/goldrail.node points the loader at a specific build. It is a pin, not a hint — when it is set, nothing else is tried, so an operator who names a binary always gets that binary.

npm test compiles the suite before running it, so it works on every Node this package supports rather than only on one new enough to strip types. The end-to-end suite drives the real addon and skips with a printed notice when it has not been built — never silently.

License

Apache-2.0