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

@celo/buy-gateway

v0.8.5

Published

buy gateway — serve an x402 paywall from a declarative YAML config

Readme

@celo/buy-gateway

Declarative x402 paywall gateway for Celo stablecoin payments.

Turns a YAML configuration file (paywall.yml) into an authenticated HTTP proxy server that intercepts requests, returns 402 Payment Required challenges, verifies EIP-3009 transfer authorizations (and optional Self.xyz KYC proofs), settles payments on Celo, and proxies requests upstream.

Features

  • Multi-Service Routing: Route distinct path prefixes to different upstream backends (e.g. Alchemy, Dune, custom AI backends).
  • Prefix Stripping (stripPrefix): Map /alchemy/v1/query to upstream /v1/query.
  • Granular Rate Limiting: Enforce token-bucket rate limits per client IP (scope: ip), per paying wallet (scope: payer), or per upstream provider (scope: service).
  • HTTP Method & Body Caps: Restrict permitted HTTP verbs (methods: [GET, POST]) and enforce maximum body payloads (maxBodyBytes).
  • JSON Body Checks: Refuse a body that is not JSON (json: true), one whose top-level fields are outside an allowlist of values (jsonFields: { model: [some-model] }), or one that carries a field you did not list (jsonOnlyFields: [model, messages], which also refuses a query string), with a 400 before the 402, so a request the upstream would reject or that asks for something you do not sell is never charged.
  • Text-Only Messages: For a chat-style route, textOnlyMessages: true requires each messages[] entry to carry only role, content and name, with content a string or an array of {type: "text", text} parts. An image, audio or file part is refused with a 400 before the 402. Such a part is a few dozen body bytes, but the provider fetches it and bills thousands of prompt tokens, so without this check maxBodyBytes does not bound what a flat-priced call costs you. Implies json; the body is forwarded as it was checked. TEXT_ONLY_MESSAGES_SCHEMA is the matching JSON Schema for a published input schema.
  • Forwarded Headers: List the only caller headers a proxy service passes on (forwardHeaders: [content-type, accept]), so a provider's own control headers cannot ride along. Injected credentials are sent either way.
  • JSON Batch Caps: Bound entries in a top-level JSON array (maxJsonArrayLength: 2000) before payment, including JSON-RPC notifications. Implies valid JSON; single requests and accepted batches keep their original bytes.
  • Soft-KYC Gates: Cross-bind payment signatures to Self.xyz zero-knowledge passport proofs (scope, minimumAge, ofac, excludedCountries).

Configuration Reference

name: my-gateway
version: 1
network: celo-sepolia
payTo: "0x0000000000000000000000000000000000C0FFEE"

# Behind a reverse proxy (Cloud Run, an ALB, Cloudflare), set this so IP-scoped
# limits key on the address the proxy appended to X-Forwarded-For — the LAST
# entry — rather than on the proxy's own address, which every caller would
# share. Leave it unset when the gateway is reached directly: with it set, a
# client-supplied X-Forwarded-For would be believed.
trustProxy: true

# Global rate limit across all incoming requests. Runs before any payment is
# read, so its only enforceable scope is `ip`.
rateLimit:
  scope: ip
  requestsPerMinute: 300
  burst: 50

# Named Upstream Services
services:
  alchemy:
    mode: proxy
    upstream: https://eth-sepolia.g.alchemy.com
    timeoutMs: 10000
    injectHeaders:
      Authorization: ALCHEMY_API_KEY
    rateLimit:
      scope: service
      requestsPerMinute: 120
      burst: 20

  dune:
    mode: proxy
    upstream: https://api.dune.com
    # Covers the whole upstream answer, body included (default 30000). The
    # body is also capped at 10 MiB decoded; past either limit the caller gets
    # a 504 or 502 that still carries the payment receipt. A caller who
    # disconnects aborts the upstream request.
    timeoutMs: 30000
    injectHeaders:
      X-DUNE-API-KEY: DUNE_API_KEY
    rateLimit:
      scope: payer
      requestsPerMinute: 30

  mock-ai:
    mode: respond
    contentType: application/json
    body: '{"status":"ok","output":"Synthetic model output"}'

# Endpoints & Allowlist
endpoints:
  - path: /healthz
    service: mock-ai
    free: true
    restrictions:
      methods: [GET]

  - path: /alchemy/*
    service: alchemy
    stripPrefix: /alchemy
    price: 0.001 USDC
    restrictions:
      methods: [POST, GET]
      maxBodyBytes: 32768

  - path: /dune/*
    service: dune
    stripPrefix: /dune
    price: 0.05 USDC
    self:
      scope: buy-dune
      minimumAge: 18
      ofac: true

Where each rateLimit.scope is enforced

A scope is only accepted where the server consults it; anywhere else the config is refused at boot rather than silently ignored.

| level | ip | payer | service | |---|---|---|---| | top-level rateLimit | ✓ | — | — | | services.<name>.rateLimit (and routing.rateLimit) | ✓ | ✓ (priced endpoints only) | ✓ | | endpoint rateLimit (priced) | ✓ | ✓ | — | | endpoint rateLimit (free: true) | ✓ | — | — |

ip limits count every request, including the unpaid 402 probe, so a priced endpoint with burst: 1 can never be bought: the probe spends the token and the paid retry is refused. Size burst for probe + retry.

A service-level payer limit is consulted after payment verification, so a service with a free: true endpoint cannot carry one: it is refused at boot rather than silently letting free calls through.

service limits count only requests that reach the upstream — a free one when it is served, a paid one after verification and before settlement (a refusal there hands the payer's nonce back, so the same signed payment can be retried). The 402 probe does not count: it never reaches the upstream, and a bucket it could drain would let anyone throttle every paying customer without paying.

Two things the protocol does not do

A payment is not bound to the endpoint it was quoted for. EIP-3009 signs the payer, payee, amount and nonce — not the resource. Verification checks the same fields. So two endpoints on the same network, token, payee and price are interchangeable: a challenge obtained at one can be paid at the other. Price is the only thing that separates a cheap service from an expensive one; give them different prices if that matters.

Settlement precedes the upstream call. That is the x402 contract — pay, then serve — so an upstream that is down after settlement still costs the payer, and so does any status the upstream answers with, a 400 or a 429 included. Only the gateway's own refusals (404, 405, 413, its rate limits, and the json, jsonFields, jsonOnlyFields, textOnlyMessages, and maxJsonArrayLength checks) come before the 402. The gateway returns the transaction and X-PAYMENT-RESPONSE on that 502/504 so there is something to reconcile with. buy gate probes every proxy service's upstream with that service's own injectHeaders at boot, so a stale key is a boot-time refusal naming the header rather than a paid 401.

The gateway does not refund. A host that does uses onComplete (below): the buy API gateway logs every paid call that settled and was not served, records a refund owed for it keyed by the settlement tx, and confirms the operator's refund transfer on chain. See examples/paywall-refunds.mjs and "Paid but not served" in docs/api-gateway.md in the repository.

Knowing what a paid request delivered

createGateway({ ..., onComplete }) calls back once per paid request, after the response is written, with the settlement and the outcome:

createGateway({
  config,
  facilitator,
  onComplete: (e) => {
    // e.transaction: the settlement tx (null in mock mode)
    // e.status, e.delivered, e.reason: what the caller got
    // e.payer, e.payTo, e.asset, e.amount, e.endpoint, e.correlationId, ...
    if (e.transaction && e.delivered) recordJob(e);
  },
});

delivered is true when the resource reached the caller, which includes an upstream 4xx or 5xx relayed as is (the payer pays for those too, see above). It is false when the gateway wrote its own 502/504 (reason: upstream_unreachable, upstream_timed_out, upstream_redirected, upstream_body_failed, upstream_body_timed_out, upstream_response_too_large, serve_failed) or the caller hung up (caller_disconnected, status: null). It never fires for a 402, a failed verify or settle, a rate limit, a 503, or a free endpoint. The callback runs without being awaited. If it throws or rejects, the gateway logs onComplete threw or onComplete rejected and the response is unaffected.