# A small app that sells with sub3 A plain Node server and page (Node 22.13+, one dependency: `jose`). It shows every part of an integration: - wallet sign-in, as a popup or a redirect, and server-side JWT verification; - checkout links built from the product (`acme/app`) and an item key; - access from `GET /v1/access`, cached 30 seconds and read fresh after a checkout; - tiers: Basic (level 1, 10 actions a month) and Pro (level 2, 100 a month and a Pro feature), renewals and the upgrade from Basic to Pro, with the monthly allowance tracked per `cycle.id`; - a lifetime item (`forever`) that also unlocks the Pro feature; - credit packs credited once per receipt from `GET /v1/orders`, including payments whose result was lost. ## Run it Create the product first, with the CLI ([../create-product.md](../create-product.md)): ```sh node sub3.mjs product create acme/app --name "Acme App" \ --tier basic:1:5:Basic --tier pro:2:20:Pro --lifetime forever:99:Forever --pack credits:5:1000:"1000 credits" ``` Then: ```sh cd examples/node npm ci cp .env.example .env # set SUB3_PRODUCT to your handle/key npm start # http://localhost:3317 ``` `.env.example` uses the sandbox (Arc Testnet, free test USDC from https://faucet.circle.com). Use `SUB3_ENVIRONMENT=production` only with a production product. `APP_ORIGIN` must be the exact origin the browser shows: session tokens are issued to it. If your item keys differ, edit `config.mjs`. `npm test` runs the ledger and HTTP tests against a fake sub3 API. `node website/tests/e2e/demos.mjs` (from the repository root, with the local stack running: `cd website && npm run dev`) drives the real hosted pages. ## Files | File | What it does | Copy it? | | ----------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | | `public/sub3.js` | Opens `/login` and `/checkout` (popup or redirect), reads redirect results, builds checkout paths | Yes, into your frontend | | `sub3.mjs` | `verifySession`, `getAccess` (cached), `afterCheckout`, `creditPack`, `syncPacks` | Yes, into your backend | | `config.mjs` | What each item unlocks (`allowances`, `premium`, `actionCost`) and `entitlements(access)`; environment | Adapt | | `ledger.mjs` | Once-only receipts, credits, allowance used per cycle, idempotent actions, on any synchronous SQL store | Port its keys and transactions | | `sqlite.mjs` | The ledger on a SQLite file (`node:sqlite`); Cloudflare uses a Durable Object instead (`ledgerOn`) | Example | | `app.mjs` | Routes: `/api/config`, `/api/account`, `/api/refresh` (after sign-in or checkout), `/api/run`, and `/.well-known/sub3.json` | Example | | `server.mjs` | The Node HTTP server around `app.mjs` and the static pages | Example | | `public/app.js` | The page: `signIn`, `buy`, `receive`, the buttons and the developer view | Example | | `public/logo.svg` | This shop's logo, named in `/.well-known/sub3.json` | Replace with yours | The same app runs on Cloudflare Workers: see [../cloudflare](../cloudflare/README.md). ## Your logo at checkout `app.mjs` serves `/.well-known/sub3.json` with your handle (from `SUB3_PRODUCT`) and `/logo.svg`. sub3 shows the logo, `@handle` and a verified ✓ on its sign-in and checkout pages once your merchant's website is this site over HTTPS: `node sub3.mjs merchant set --url https://your.site`, then `node sub3.mjs verify `. The logo must be on the same domain. On `localhost` nothing can be verified; deploy first (for example to Cloudflare). ## What to try 1. **Sign in.** Tick "Use redirects" to try the redirect flow. An expired or foreign token is a 401; a sub3 outage is a 503 and keeps you signed in. 2. **Buy Basic for a month.** Run a few actions: each uses this cycle's allowance. The Pro feature is refused. 3. **Renew Basic, or buy 12 months.** The end date moves; the cycle and the allowance used in it do not. 4. **Upgrade to Pro.** The checkout warns that it starts now at full price and forfeits the rest of Basic. After it, `cycle.id` is new, so the 100-action allowance is available at once, and the Pro feature works. 5. **Basic while Pro is active** shows "After ": a lower tier waits until expiry. 6. **Buy a credit pack.** When the allowance runs out, actions spend credits. Close the checkout window right after paying: "Refresh" finds the payment through `/v1/orders` and credits it once, however often you refresh. 7. **Buy Forever.** It unlocks the Pro feature without a tier, for good. 8. **Make the action fail.** What it took is given back. Each action has an `Idempotency-Key`; a retried request is charged once. Not covered, on purpose: payment refunds, proration, automatic renewal and expiring credits. sub3 does not do them.