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

@atmopay/client

v0.1.0

Published

Sell subscriptions and read gated content in an ATProto app, without touching money, cards or KYC.

Readme

Integrating AtmoPay

How an ATProto app sells subscriptions and shows gated content — what Flashes does, written down.

You add subscriptions to your app without touching money, cards, or KYC — and you earn a share of what you sell and serve. There is no API key and nothing to configure: every authenticated call carries a service-auth token minted by the viewer's own PDS, so your app is saying "I am acting for this user, and their server vouches for it".

npm install @atmopay/client

The minimum: a subscribe button

Four calls. This is the whole integration if you only want to sell a subscription and unlock your own features.

import { AtmoPayClient } from '@atmopay/client'

const atmopay = new AtmoPayClient({
  baseUrl: 'https://atmopay.birdsongapps.com',
  serviceDid: 'did:web:atmopay.birdsongapps.com',
  agent,                                    // the viewer's @atproto/api Agent
  appId: 'https://yourapp.example/oauth-client-metadata.json',
})

const offer = await atmopay.getOffer(creatorDid)   // null ⇒ sells nothing; no session needed
if (offer) {
  const { active } = await atmopay.checkEntitlement(creatorDid)
  if (!active) await atmopay.subscribe(creatorDid, offer.tiers[0].id)
}

| Call | Auth | Purpose | |---|---|---| | getOffer(did) | none — public | does this account sell? render the card | | checkEntitlement(did) | viewer's token | is this viewer subscribed? | | subscribe(did, tier) | viewer's token | the button | | unsubscribe(did) | viewer's token | cancel |

Money never touches your app: subscribe posts, and AtmoPay charges on the rail server-side.

Gating your own features

The entitlement check is not creator-specific — it answers "does DID A hold a subscription to DID B". Register your own account as a merchant, add a tier, and check entitlement against your own DID. Same rail, no extra work.

Two things to know: on iOS, Apple requires IAP for in-app feature purchases; and to verify entitlement in your backend, without the user present, use the portable receipt rather than this endpoint, which only answers about its caller.


Showing gated content

This half does not talk to AtmoPay. A creator's gated posts live in their own space on their own PDS, and AtmoPay is only the doorman that PDS consults. So showing gated content is ATProto, not an integration.

import { readSpacePosts, releaseSpacePosts } from '@atmopay/client'

let posts = null
if (offer.space) posts = await readSpacePosts(agent, offer.space, creatorDid)
if (!posts) posts = await atmopay.vaultPhotos(creatorDid)   // fall back

readSpacePosts returns null — rather than throwing — whenever the viewer's PDS cannot do this, which is most of them today. Always keep the fallback: vaultPhotos reads AtmoPay's mirror, which exists precisely so that viewers without spaces still see something. The mirror goes away when spaces ship broadly; the space copy is canonical.

Under the hood it is three calls: the viewer's PDS mints a delegation token, the creator's PDS exchanges it for a DPoP-bound space credential — the moment AtmoPay's doorman is consulted — and the records and blobs follow.

Authoring gated content

For an app whose users are creators:

import { writeSpacePost } from '@atmopay/client'

const uri = await writeSpacePost(agent, offer.space, agent.did, { text, image })
await atmopay.relayGatedPhoto(image, text)   // mirror; failure is not fatal

Writing needs no credential dance — it is the creator's own PDS and their own repo, so their session carries it.


OAuth scopes

import { BASE_SCOPE, scopesFor } from '@atmopay/client'

const scope = [BASE_SCOPE, ...scopesFor(pdsUrl, { author: true })].join(' ')

Selling and checking entitlement need no special scopeatproto is enough, because the service-auth token is minted for AtmoPay specifically.

Space scopes are another matter, and the vocabulary is exact. It is enforced only by a real PDS, so a wrong value compiles, passes tests, and fails at sign-in:

  • actions are read_self | read | create | update | deletethere is no write
  • collection and manage both default to empty: what you do not name is not granted
  • authority defaults to self, so reading someone else's space needs authority=*

Ask for space scopes only on a PDS that implements spaces — a general provider rejects an unknown scope and the whole sign-in fails. scopesFor does that check.

Declare every scope you might request in your client metadata: the authorization server enforces requested ⊆ declared. And pass the scope explicitly on each sign-in rather than letting the client default to your metadata, or a spaces-only scope will follow your users to providers that cannot grant it.


Getting paid

A share of each payment is pooled for the apps that sold and served it, split between origination (who converted the sale) and delivery (who actually put the content in front of the supporter, metered per period).

  • Origination comes from the appId you pass — self-asserted, because a browser app has no secret to sign with.
  • Delivery is attested by the viewer's PDS, which tells AtmoPay's doorman which client is asking. You do nothing for this; it is measured.

Accrual needs only a client_id, so you earn from your first integration with no paperwork. Withdrawal needs onboarding as a payee — business verification and a bank account — because paying an unidentified party is not something a regulated payment provider may do.