Skip to content
Paykit
Esc
↑↓navigate↵open⌘Jpreview
On this page

Getting started

Install Paykit, declare two plans, deploy them, subscribe an account, and record usage.

This walkthrough builds two plans, a subscription and the check and track loop on the in-memory store. It needs no database and no provider key, and it ends with a Worker that answers a check from local state.

Prerequisites

Install

npm install @potion.run/paykit alchemy effect
pnpm add @potion.run/paykit alchemy effect
yarn add @potion.run/paykit alchemy effect
bun add @potion.run/paykit alchemy effect
nub add @potion.run/paykit alchemy effect
aube add @potion.run/paykit alchemy effect

Declare your pricing

A feature is what a plan meters. A plan grants an allowance of it, and a price makes the plan sellable. This file is the whole declaration:

import { Paykit } from "@potion.run/paykit";

export const messages = Paykit.feature({ id: "messages", name: "Messages" });

export const free = Paykit.plan({
  id: "free",
  name: "Free",
  grants: [Paykit.planItem(messages, { included: 100 })],
});

export const pro = Paykit.plan({
  id: "pro",
  name: "Pro",
  price: { amount: 1900, currency: "usd", interval: "month" },
  grants: [Paykit.planItem(messages, { included: 2_000, overage: "bill" })],
});

export const catalog = { features: [messages], tiers: [free, pro] };

Money is in minor units: 1900 is $19.00. See Plans and prices for resets, rollover, caps and variants.

Declare the billing stack

One object names the catalog, the providers and the store. The deploy, the Worker and the tests all read it:

import { Paykit, type BillingStackProps } from "@potion.run/paykit";
import { catalog } from "./catalog.ts";

export const Provider = Paykit.Manual();

export const billing = {
  catalog,
  providers: [Provider],
  store: Paykit.MemoryStore(),
} satisfies BillingStackProps;

Manual sells through an invoice, so the walkthrough needs no provider key. MemoryStore keeps the balances in the process, which suits a quickstart and does not survive a restart. Database swaps in D1, Postgres or MySQL.

Deploy it

Paykit.BillingStack validates the catalog and records it in stack state:

import * as Cloudflare from "@alchemy.run/cloudflare";
import { Paykit } from "@potion.run/paykit";
import * as Alchemy from "alchemy";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import { billing } from "./src/billing.config.ts";
import Api from "./src/worker.ts";

export default Alchemy.Stack(
  "MyApp",
  {
    providers: Layer.mergeAll(Cloudflare.providers(), Paykit.providers()),
    state: Alchemy.localState(),
  },
  Effect.gen(function* () {
    const recorded = yield* Paykit.BillingStack("Billing", billing);
    const api = yield* Api;

    return { url: api.url.as<string>(), tiers: recorded.tiers };
  }),
);
alchemy deploy

Alchemy shows a plan for the Billing resource and the Worker, then applies it. A catalog with a plan that grants an undeclared feature stops the deploy here.

Serve the calls

Paykit.world builds the runtime from the same declaration. Add the guards your deployment uses. The development guards read an actor from headers and are named to warn you:

import { InsecureDevGuards, Paykit } from "@potion.run/paykit";
import { billing } from "./billing.config.ts";

export const Billing = Paykit.world({ ...billing, guards: InsecureDevGuards });

Paykit.handler mounts the tenant API, the operator API, the checkout path and the webhook path. The Worker returns the handler’s own fetch:

import * as Cloudflare from "@alchemy.run/cloudflare";
import { Paykit } from "@potion.run/paykit";
import * as Effect from "effect/Effect";
import { billing, Provider } from "./billing.config.ts";
import { Billing } from "./billing.ts";

export default class Api extends Cloudflare.Worker<Api>()(
  "Api",
  { main: import.meta.url },
  Effect.gen(function* () {
    const handler = yield* Paykit.handler({
      provider: Provider,
      accountOf: (request) => Paykit.accountFromHeaders(request.headers),
      surfaces: "tenant",
    });

    return { fetch: handler.fetch };
  }).pipe(Effect.provide(Billing), Effect.orDie),
) {}

Subscribe an account

An account with no subscription is refused with an empty balance. Subscribe it to the free plan. A free plan activates immediately and answers { activated: true }:

const subscribe = yield* Paykit.Subscribe(account);
const result = yield* subscribe({ tier: "free" });

result; // { activated: true }

A priced plan returns a checkout URL or an invoice id instead, and activates when the provider confirms the payment. Build a billing page shows the whole purchase path.

Check, then track

With the subscription active, the same check answers from the free plan’s allowance:

const check = yield* Paykit.Check(account);
const { allowed, balance } = yield* check({ featureId: "messages" });

allowed; // true
balance.remaining; // 100

Record what the work consumed. The key makes a retry the same fact, not a second charge:

const track = yield* Paykit.TrackUsage(account);
const after = yield* track({
  featureId: "messages",
  value: 1,
  idempotencyKey: requestId,
});

after.balance.remaining; // 99

That is the loop: declare, deploy, subscribe, check, track. The same calls run on D1 or Postgres, and the same catalog sells through Stripe or Polar.

Where next

Was this page helpful?