{"_id":"@atmopay/client","name":"@atmopay/client","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@atmopay/client","version":"0.1.0","description":"Sell subscriptions and read gated content in an ATProto app, without touching money, cards or KYC.","license":"MIT","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc -p tsconfig.json","typecheck":"tsc -p tsconfig.json --noEmit","prepublishOnly":"npm run build"},"peerDependencies":{"@atproto/api":"*"},"devDependencies":{"typescript":"^5.9.3"},"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/birdsongapps/atmopay.git"},"keywords":["atproto","bluesky","payments","subscriptions","spaces"],"gitHead":"bcd60a195f233409f591abeda37f6479c5664a54","bugs":{"url":"https://github.com/birdsongapps/atmopay/issues"},"homepage":"https://github.com/birdsongapps/atmopay#readme","_id":"@atmopay/client@0.1.0","_nodeVersion":"24.15.0","_npmVersion":"8.5.1","dist":{"integrity":"sha512-IKJStN+sW4p01vFxK2iHVxpJQwrxhnjyVHtINoOhTCupAsiS0yG0751SDc7tx+wFsU/Qgv554KK68TkHcAnp5g==","shasum":"a557879bb0a25fc9e573db4250677a41d7cb8853","tarball":"https://registry.npmjs.org/@atmopay/client/-/client-0.1.0.tgz","fileCount":10,"unpackedSize":34899,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQC3XbPIz+jeEUNflPwZ/uQjPsdv264qDhYoQ5gEhkhvJgIgE9ZSyekEUR48x9tPdnZoNm7Bo3J4B+UVphbZ2Fk8CIg="}]},"_npmUser":{"name":"seboslaw","email":"seboslaw+npm@gmail.com"},"directories":{},"maintainers":[{"name":"seboslaw","email":"seboslaw+npm@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/client_0.1.0_1788369765769_0.08246067298218329"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-02T17:22:45.459Z","0.1.0":"2026-09-02T17:22:45.906Z","modified":"2026-09-02T17:22:46.234Z"},"maintainers":[{"name":"seboslaw","email":"seboslaw+npm@gmail.com"}],"description":"Sell subscriptions and read gated content in an ATProto app, without touching money, cards or KYC.","homepage":"https://github.com/birdsongapps/atmopay#readme","keywords":["atproto","bluesky","payments","subscriptions","spaces"],"repository":{"type":"git","url":"git+https://github.com/birdsongapps/atmopay.git"},"bugs":{"url":"https://github.com/birdsongapps/atmopay/issues"},"license":"MIT","readme":"# Integrating AtmoPay\n\n*How an ATProto app sells subscriptions and shows gated content — what Flashes\ndoes, written down.*\n\nYou add subscriptions to your app without touching money, cards, or KYC — and\nyou earn a share of what you sell and serve. There is **no API key** and\nnothing to configure: every authenticated call carries a service-auth token\nminted by the *viewer's own PDS*, so your app is saying \"I am acting for this\nuser, and their server vouches for it\".\n\n```bash\nnpm install @atmopay/client\n```\n\n---\n\n## The minimum: a subscribe button\n\nFour calls. This is the whole integration if you only want to sell a\nsubscription and unlock your own features.\n\n```ts\nimport { AtmoPayClient } from '@atmopay/client'\n\nconst atmopay = new AtmoPayClient({\n  baseUrl: 'https://atmopay.birdsongapps.com',\n  serviceDid: 'did:web:atmopay.birdsongapps.com',\n  agent,                                    // the viewer's @atproto/api Agent\n  appId: 'https://yourapp.example/oauth-client-metadata.json',\n})\n\nconst offer = await atmopay.getOffer(creatorDid)   // null ⇒ sells nothing; no session needed\nif (offer) {\n  const { active } = await atmopay.checkEntitlement(creatorDid)\n  if (!active) await atmopay.subscribe(creatorDid, offer.tiers[0].id)\n}\n```\n\n| Call | Auth | Purpose |\n|---|---|---|\n| `getOffer(did)` | none — public | does this account sell? render the card |\n| `checkEntitlement(did)` | viewer's token | is this viewer subscribed? |\n| `subscribe(did, tier)` | viewer's token | the button |\n| `unsubscribe(did)` | viewer's token | cancel |\n\nMoney never touches your app: `subscribe` posts, and AtmoPay charges on the\nrail server-side.\n\n### Gating your own features\n\nThe entitlement check is not creator-specific — it answers \"does DID *A* hold a\nsubscription to DID *B*\". Register **your own** account as a merchant, add a\ntier, and check entitlement against your own DID. Same rail, no extra work.\n\nTwo things to know: on iOS, Apple requires IAP for in-app feature purchases;\nand to verify entitlement in *your backend*, without the user present, use the\nportable receipt rather than this endpoint, which only answers about its caller.\n\n---\n\n## Showing gated content\n\nThis half does **not** talk to AtmoPay. A creator's gated posts live in their\nown space on their own PDS, and AtmoPay is only the doorman that PDS consults.\nSo showing gated content is ATProto, not an integration.\n\n```ts\nimport { readSpacePosts, releaseSpacePosts } from '@atmopay/client'\n\nlet posts = null\nif (offer.space) posts = await readSpacePosts(agent, offer.space, creatorDid)\nif (!posts) posts = await atmopay.vaultPhotos(creatorDid)   // fall back\n```\n\n`readSpacePosts` returns `null` — rather than throwing — whenever the viewer's\nPDS cannot do this, which is most of them today. **Always keep the fallback**:\n`vaultPhotos` reads AtmoPay's mirror, which exists precisely so that viewers\nwithout spaces still see something. The mirror goes away when spaces ship\nbroadly; the space copy is canonical.\n\nUnder the hood it is three calls: the viewer's PDS mints a delegation token,\nthe creator's PDS exchanges it for a DPoP-bound space credential — the moment\nAtmoPay's doorman is consulted — and the records and blobs follow.\n\n## Authoring gated content\n\nFor an app whose users are creators:\n\n```ts\nimport { writeSpacePost } from '@atmopay/client'\n\nconst uri = await writeSpacePost(agent, offer.space, agent.did, { text, image })\nawait atmopay.relayGatedPhoto(image, text)   // mirror; failure is not fatal\n```\n\nWriting needs no credential dance — it is the creator's own PDS and their own\nrepo, so their session carries it.\n\n---\n\n## OAuth scopes\n\n```ts\nimport { BASE_SCOPE, scopesFor } from '@atmopay/client'\n\nconst scope = [BASE_SCOPE, ...scopesFor(pdsUrl, { author: true })].join(' ')\n```\n\nSelling and checking entitlement need **no special scope** — `atproto` is\nenough, because the service-auth token is minted for AtmoPay specifically.\n\nSpace scopes are another matter, and the vocabulary is exact. It is enforced\nonly by a real PDS, so a wrong value compiles, passes tests, and fails at\nsign-in:\n\n- actions are `read_self | read | create | update | delete` — **there is no `write`**\n- `collection` and `manage` both default to **empty**: what you do not name is not granted\n- `authority` defaults to `self`, so reading *someone else's* space needs `authority=*`\n\nAsk for space scopes **only on a PDS that implements spaces** — a general\nprovider rejects an unknown scope and the whole sign-in fails. `scopesFor`\ndoes that check.\n\nDeclare every scope you might request in your client metadata: the\nauthorization server enforces requested ⊆ declared. And pass the scope\nexplicitly on each sign-in rather than letting the client default to your\nmetadata, or a spaces-only scope will follow your users to providers that\ncannot grant it.\n\n---\n\n## Getting paid\n\nA share of each payment is pooled for the apps that sold and served it, split\nbetween **origination** (who converted the sale) and **delivery** (who actually\nput the content in front of the supporter, metered per period).\n\n- Origination comes from the `appId` you pass — self-asserted, because a\n  browser app has no secret to sign with.\n- Delivery is attested by the viewer's PDS, which tells AtmoPay's doorman which\n  client is asking. You do nothing for this; it is measured.\n\nAccrual needs only a client_id, so **you earn from your first integration with\nno paperwork**. Withdrawal needs onboarding as a payee — business verification\nand a bank account — because paying an unidentified party is not something a\nregulated payment provider may do.\n","readmeFilename":"README.md","_rev":"1-9b71a458c53cd0bb6dca29845bff6563"}