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

@happyvertical/payments

v0.80.3

Published

Payment backend abstraction with adapters for Base USDC, BTCPay Server, and Stripe

Readme

@happyvertical/payments

Multi-backend payment provider abstraction for crypto and fiat checkout.

The root import exposes the shared PaymentBackend contract, common types, errors, and dynamic factory:

import { createPaymentBackend } from '@happyvertical/payments';

const payments = await createPaymentBackend({
  type: 'stripe',
  secretKey: process.env.STRIPE_SECRET_KEY!,
  successUrl: 'https://example.com/pay/success',
  cancelUrl: 'https://example.com/pay/cancel',
});

Adapters are also available as selective subpath exports:

import { BaseUsdcAdapter } from '@happyvertical/payments/base-usdc';
import { BtcAdapter } from '@happyvertical/payments/btc';
import { StripeAdapter } from '@happyvertical/payments/stripe';

Adapters

  • BaseUsdcAdapter: USDC on Base via JSON-RPC, deterministic xpub child addresses, x402 proof verification, and injected signer payouts.
  • BtcAdapter: BTCPay Server invoice, polling/webhook status, tiered confirmation policy, and unsigned PSBT payout creation.
  • StripeAdapter: Stripe Checkout URL settlement, webhook verification, refunds, Stripe Connect transfers, saved payment methods / card-on-file (createSetupSession / getSetupResult), and the manual-capture card lifecycle (authorizePayment / capturePayment / voidPayment).

The package does not depend on SMRT or a database. Consumers own quote persistence, webhook routing, and operational policy.

Save a card (setup) → charge later

const setup = await stripe.createSetupSession({
  customerEmail: advertiser.email, // or providerCustomerId for an existing customer
  successUrl,
  cancelUrl,
});
redirect(setup.url); // buyer saves their card on Stripe's hosted page

// on return, read the reusable references to persist:
const saved = await stripe.getSetupResult({ sessionId: setup.sessionId });
if (saved.status === 'complete') {
  // both refs are guaranteed present when complete; persist them (never `raw`,
  // which may contain PII). Charge later via authorizePayment (see below /
  // the manual-capture adapter).
  persist({ cus: saved.providerCustomerId, pm: saved.providerPaymentMethodId,
            brand: saved.brand, last4: saved.last4 });
}
// status: 'pending' (still processing), 'failed' (declined — stop polling),
// 'expired' (session lapsed). A setup-mode Checkout webhook is only a
// "go call getSetupResult" trigger, not the save outcome itself.

Manual capture (authorize → capture / void)

Charge a saved card later — e.g. hold at approval, capture on fulfillment:

const auth = await stripe.authorizePayment({
  amount: 5000, // minor units
  currency: 'CAD',
  providerPaymentMethodId: saved.providerPaymentMethodId, // pm_...
  providerCustomerId: saved.providerCustomerId, // cus_... — REQUIRED for a saved card
  quoteId: campaign.id, // stamp your id so capture/void webhooks are correlatable
  // offSession defaults to true (buyer absent). An off-session step-up throws
  // `authentication_required`; re-prompt on-session (offSession: false) to recover.
});
if (auth.status !== 'requires_capture') {
  // 'succeeded' means already captured (don't capture again); else declined.
}
await stripe.capturePayment({ providerPaymentId: auth.providerPaymentId, idempotencyKey });
// or, to release the hold:
await stripe.voidPayment({ providerPaymentId: auth.providerPaymentId, idempotencyKey });

Common footguns: omitting providerCustomerId for a customer-attached (saved) card 400s at charge time; omitting quoteId leaves the intent's capture/void/fail webhooks un-correlatable (processing, no quoteId).

Public payment amounts follow Stripe's convention: amount is an integer in the smallest currency unit, paired with a currency code. For example, { amount: 1234, currency: 'USD' } means USD 12.34. Crypto settlement uses the same rule at the public boundary: { amount: 10000, currency: 'BTC' } means 10,000 satoshis, and { amount: 1000000, currency: 'USDC' } means 1 USDC. Adapters convert those integers to provider-specific decimal strings or atomic unit strings internally when a protocol requires it.

Stripe-compatible whole-unit currencies that still have two-decimal ISO minor units, such as ISK and UGX, must be passed in whole-unit increments: { amount: 500, currency: 'UGX' } means UGX 5.00, and amount must be evenly divisible by 100.

Operational Notes

Migration Notes

Webhook parsing now requires configured secrets. Callers that previously parsed Stripe or BTCPay webhooks without webhookSecret must configure the provider secret or handle trusted test fixtures outside parseWebhookEvent.

Webhook parsing is fail-closed. StripeAdapter.parseWebhookEvent and BtcAdapter.parseWebhookEvent require a configured webhookSecret and a valid signature. BTCPay webhook payloads must include deliveryId; the exported BtcpayWebhookEvent.deliveryId type is required. Duplicate webhook detection is bounded, in-memory replay protection for the current adapter instance; production webhook handlers should still persist Stripe event ids and BTCPay delivery ids before applying side effects, especially across restarts or multiple replicas.

Base x402 receipt replay protection is also bounded and in-memory. Persist the accepted transaction id in the application layer if a verified x402 proof unlocks durable access.

StripeAdapter sends Checkout Sessions with Stripe API-compatible expiry values only. Expiries outside Stripe's 30 minute to 24 hour Checkout window are rejected instead of being silently clamped.

BtcAdapter defaults to the BTCPay on-chain method id BTC-CHAIN while still matching exact BTC and BITCOIN aliases.

BaseUsdcAdapter records metadata.searchStartBlock on created payment options. Persist it with quote state and pass it back as PaymentStatusContext.searchStartBlock after process restarts so old transfers cannot satisfy a new quote. Stateless getStatus calls that provide an amount must also provide searchStartBlock unless the adapter was configured with fromBlock. Long-lived unpaid quotes may force eth_getLogs ranges that exceed some RPC provider limits; use an archive provider, provider-supported range, or application-level batching for old searchStartBlock values. Cumulative split-transfer matching is disabled by default; enable allowCumulativeTransferMatching only when every quote receives a unique deposit address and the search start block is persisted. When quote currency is USD, Base settlement assumes a 1:1 USD-to-USDC amount; callers that need live FX or spread handling should convert to USDC amounts before creating payment options.

pollPaymentStatus emits event: 'timeout' when polling times out. The associated status remains the last observed status, or pending if no status was observed, rather than being coerced to failed.