Polar
Sell the catalog through Polar as merchant of record, with products and meters the deploy creates.
Polar sells as merchant of record: it calculates and collects tax, hosts the checkout and keeps the subscription. Paykit creates the products and meters the catalog states and records what Polar collects.
Deploy the vendor objects
PolarBilling creates a product per sold plan and a meter per metered feature.
It returns the provider the billing stack validates, plus the ids the Worker
needs:
import { PolarBilling, polarProviders } from "@potion.run/paykit-polar";
import * as Layer from "effect/Layer";
const providers = Layer.mergeAll(Cloudflare.providers(), Paykit.providers()).pipe(
Layer.provideMerge(polarProviders({ server: "sandbox" })),
);
const billing = yield* PolarBilling("PolarBilling", {
server: "sandbox",
origin: "https://app.example.com",
tiers: polarTiers,
});
const recorded = yield* Paykit.BillingStack("Billing", {
catalog: billingCatalog,
providers: [billing],
});
The deploy’s token comes from POLAR_API_KEY, or from the profile
ALCHEMY_PROFILE selects (alchemy profile edit --provider Polar). Nothing is
copied into the stack, and server: "sandbox" pins the base URL so a
production token cannot create production objects.
polarTiers repeats the catalog’s prices in the shape Polar sells them:
{ pro: { price: proPrice, usage: { messages: messagePrice } } }.
billing.sold carries the ids: sold.products.pro and sold.meters.messages.
billing.accessToken is the token the deploy resolved; bind it with the ids
into the Worker’s environment as POLAR_API_KEY.
billing.webhookEndpoint is the endpoint the deploy created: resource is the
vendor object, and secret is the signing key Polar minted at create. Bind the
secret into the Worker’s environment — Polar never returns it again, so the
endpoint’s state is the only copy:
const worker = yield* Cloudflare.Worker("Worker", {
main: "./src/Worker.ts",
env: {
POLAR_API_KEY: billing.accessToken,
POLAR_WEBHOOK_SECRET: billing.webhookEndpoint.secret,
},
});
Build the runtime provider
The runtime provider takes the product id and the meter name from the environment:
import { Polar } from "@potion.run/paykit-polar";
import * as Redacted from "effect/Redacted";
export const provider = Polar({
accessToken: env.POLAR_API_KEY,
server: "sandbox",
origin: "https://app.example.com",
tiers: {
pro: {
price: proPrice,
productId: env.POLAR_PRODUCT_PRO,
usage: { messages: messagePrice },
},
},
meters: { messages: env.POLAR_MESSAGES_METER },
webhookSecret: Redacted.make(env.POLAR_WEBHOOK_SECRET),
});
The example pins server: "sandbox", so a production token cannot create
production objects from it. A live deployment states its own server.
Deliveries
PolarBilling creates the endpoint at ${origin}/webhooks/polar —
webhookPath moves it — and subscribes it to exactly the events the provider
decodes, so the vendor’s event list cannot drift from what paykit handles. A
delivery is verified with the secret the endpoint minted, decoded by the
provider, and applied as a command. The account becomes active on the delivery,
not on the redirect from checkout.
Destroying the stack deletes the endpoint: it is configuration, not financial
history, and a retired deployment must stop receiving deliveries. A deployment
that already owns one — a shared API, a secret in a vault — passes
webhookSecret instead, and nothing is created.
client is for a test only. A deployment omits it, and Polar builds its own
client from the token.
See Credits for a plan that grants a pool, and the Polar example for the whole loop: a deploy, a checkout, a signed delivery and a test that needs no Polar account.
The pages are Polar, PolarBilling, and PolarPrices.