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

@ferry402/sdk

v0.2.1

Published

Accept x402 payments from users on other chains. Non-custodial escrow settlement with Hedera Consensus Service as the clearing ledger.

Readme

@ferry402/sdk

Charge for an API call before your Express route handles it. @ferry402/sdk is an x402 middleware — x402 is an open standard that uses HTTP's 402 Payment Required status to ask a caller for a stablecoin payment, then lets the request through once that payment is verified. Each payment settles into a non-custodial Escrow smart contract (one per chain) and is journaled to Hedera, used here as a clearing ledger: an ordered, independently-auditable record of who paid whom.

This package is ferry402's merchant-side (and payer-side) half. See the repo root README for the full system — this package, the facilitator, and the contracts.

Install

npm install @ferry402/sdk

ESM only ("type": "module", no require condition). Requires Node >= 20.18.3 and Express ^4.18.0 || ^5.0.0 as a peer dependency.

Quickstart: guard a route

import express from 'express'
import { ferry402 } from '@ferry402/sdk'
import type { Ferry402Config } from '@ferry402/sdk'

const config: Ferry402Config = {
  price: '$0.01',
  accept: ['base-sepolia'],
  settleTo: 'hedera',
  merchant: '0.0.123456', // your Hedera account id
  // merchantEvm/escrows/assets are Partial<Record<SupportedChain, ...>> - an
  // entry is only required for each chain you actually list in `accept`.
  // ferry402(config) throws at construction if one is missing.
  merchantEvm: { 'base-sepolia': '0xYourPayoutAddress' },
  facilitator: 'http://localhost:4000',
  escrows: { 'base-sepolia': '0xDeployedEscrowAddress' },
  assets: { 'base-sepolia': '0xUSDCAddress' },
  secret: process.env.FERRY402_SECRET!, // >= 32 bytes, shared across every instance
}

const app = express()

app.get('/premium-endpoint', ferry402(config), (_req, res) => {
  res.json({ data: 'this only serves once payment verifies' })
})

An unpaid request gets a 402 with an accepts array; a request carrying a valid X-PAYMENT header falls through to your handler via next().

ferry402() only verifies — it does not call /settle. Actually collecting the payment and journaling it to HCS is your handler's job, immediately after next() runs. See the root README's "How it works" and @ferry402/facilitator (the companion service that verifies and settles payments on-chain) for the settlement half of that wiring.

Paying a route: createPaymentHeader

No stock x402 client (x402-fetch, x402-axios, or anything built against plain [email protected]) can pay a ferry402 route. ferry402 binds the EIP-3009 authorization's nonce to the merchant it was issued for — nonce = keccak256(abi.encode(merchantEvm, paymentId)) — instead of letting the payer mint random bytes. This way a signed payment can never be redirected to a merchant other than the one who issued the 402.

Upstream x402 clients generate a random nonce instead, so ferry402 never recognizes it as matching any challenge it issued. The payment fails closed with invalid_payment: the server has no way to tell a self-invented nonce apart from a genuinely expired or mismatched one, so it doesn't claim to.

Use createPaymentHeader instead — it derives the same nonce ferry402 expects and signs the ReceiveWithAuthorization for you:

import { createPaymentHeader } from '@ferry402/sdk'
import { privateKeyToAccount } from 'viem/accounts'

// 1. Hit the route with no payment; read the 402's `accepts` array.
const res = await fetch('http://merchant.example/premium-endpoint')
const { accepts } = await res.json()
const requirement = accepts[0] // pick the chain you want to pay on

// 2. Sign a payment header against it.
const account = privateKeyToAccount(process.env.PAYER_PRIVATE_KEY as `0x${string}`)
const header = await createPaymentHeader(requirement, account, {
  tokenName: 'USDC',   // the asset's EIP-712 domain name — read it from the
  tokenVersion: '2',   // token contract, or your deployment notes; it
  chainId: 84532,      // varies by deployment and can't be assumed
})

// 3. Retry with the header.
const paid = await fetch('http://merchant.example/premium-endpoint', {
  headers: { 'X-PAYMENT': header },
})

signer is any object with an address and a viem-compatible signTypedData — a viem PrivateKeyAccount (as above) works directly; no viem dependency is required by this package itself.

API reference

Merchant side

| Export | Signature | |---|---| | ferry402 | (config: Ferry402Config, options?: Ferry402Options) => RequestHandler | | buildRequirements | (config, resource: string, options?: BuildRequirementsOptions) => PaymentRequirements[] | | parsePrice | (price: string) => string — "$0.01" to atomic units ("10000") | | InMemoryConsumedNonceStore | class implements ConsumedNonceStore — the default, per-process replay store | | Ferry402Locals | type for res.locals.x402 once a payment verifies — see below |

ferry402() is the whole integration for most users. buildRequirements is exported so you can issue a 402 yourself (a non-Express framework, say) without reimplementing the accepts array.

res.locals.x402 (Ferry402Locals). Once a payment verifies, your route handler can read res.locals.x402 as Ferry402Locals: { payload, requirements, payer, release }. release(): Promise<void> lets the handler free the nonce ferry402 already consumed, if the handler's own /settle call then fails — call it from that failure branch; the happy path never needs it.

Payer side

| Export | Signature | |---|---| | createPaymentHeader | (requirement: PaymentRequirements, signer: EIP3009Signer, options: CreatePaymentHeaderOptions) => Promise<string> |

Nonce derivation

The binding the Escrow contract enforces on-chain, and the single TypeScript implementation of it.

| Export | Signature | |---|---| | computeNonce | (merchantEvm: Hex, paymentId: Hex) => Hex — keccak256(abi.encode(merchantEvm, paymentId)) | | normalizeNonce | (nonce: string) => Hex — canonicalise before comparison | | normalizeAddress | (address: string) => Hex — same, for addresses, so a checksum difference never changes a derivation |

computeNonce is pinned to golden vectors in both this package and the Foundry suite, so the TypeScript and Solidity derivations cannot drift apart without a test going red.

Stateless challenges

Used internally by ferry402(). Exported for anyone issuing or validating challenges outside the middleware.

| Export | Signature | |---|---| | deriveChallenge | (secret, merchantEvm, resource, nowMs?) => DerivedChallenge | | matchChallenge | (secret, merchantEvm, resource, presentedNonce, nowMs?) => DerivedChallenge \| undefined | | derivePaymentId | (secret, merchantEvm, resource, bucket) => Hex | | timeBucket | (nowMs: number, bucketSeconds?: number) => number | | generatePaymentId | () => Hex — random id, for callers not using derivation | | assertValidSecret | (secret: unknown) => asserts secret is string | | MIN_SECRET_BYTES | 32 | | TIME_BUCKET_SECONDS | 300 |

matchChallenge checks the current bucket and the previous one, so the usable payment window is up to 2 * TIME_BUCKET_SECONDS (10 minutes).

Types

Ferry402Config, Ferry402Options, Ferry402Locals, SupportedChain, PaymentRequirements, BuildRequirementsOptions, ConsumedNonceStore, EIP3009Signer, CreatePaymentHeaderOptions, DerivedChallenge.

Configuration reference

| Field | Notes | |---|---| | secret | Required, >= 32 bytes. Same value on every instance. See notes below. | | escrows | Per-chain Escrow address. Required for each chain in accept. See notes below. | | merchant | Your Hedera account id, e.g. "0.0.123456" — the clearing-layer identity the HCS journal keys on. | | merchantEvm | Per-chain payout address. Required for each chain in accept. See notes below. | | accept | Chains this merchant takes payment on. Facilitator supports only 2 of 4 values today — see notes below. | | assets | Per-chain USDC (or other supported asset) contract address. Required for each chain in accept. | | facilitator | Base URL of the facilitator this middleware calls /verify against. | | price | Decimal-dollar string, e.g. "$0.01". USDC (6 decimals) only in v1. |

  • secret — ferry402(config) throws at construction if it's missing or under 32 bytes. It must be identical across every process/instance serving this merchant: that's what lets two instances validate each other's challenges with no shared store. Generate it with openssl rand -hex 32 and never commit it.
  • escrows, merchantEvm, assets are each Partial<Record<SupportedChain, ...>> — you only need an entry for chains you actually list in accept; ferry402(config) throws at construction, naming the chain, if one is missing. This package doesn't validate escrows against a live facilitator, but note: a facilitator started with no matching escrows entry for a chain rejects every payment on that chain, fail-closed — see @ferry402/facilitator.
  • merchant vs. merchantEvm — these are different identifiers: merchant is a Hedera account id, merchantEvm is an EVM address and one of the two preimages of the nonce binding. Mixing them up breaks the nonce derivation for that chain.
  • accept — the type allows 'base' | 'base-sepolia' | 'polygon' | 'polygon-amoy', but @ferry402/facilitator only serves base and base-sepolia today. Advertising a Polygon network produces a 402 no payment can satisfy: the facilitator returns invalid_network.

Known limitations

  • Replay protection is per-process by default. InMemoryConsumedNonceStore clears on restart, losing protection for the derivation window still open at that moment, and it is not shared across horizontally-scaled instances. Pass your own ConsumedNonceStore (Redis, a database, ...) via ferry402(config, { consumedNonceStore }) for a multi-instance deployment.
  • ferry402() verifies; it does not settle. Calling /settle and writing the HCS journal entry is your handler's job, immediately after next().
  • Keep settlements sequential per facilitator. See docs/troubleshooting.md.
  • Testnet, and unaudited. See SECURITY.md.

More

License

MIT — see LICENSE.