Drop in a meter call, gate Pro features, sell credit packs. PayKit runs the ledger and Stripe takes the money — no billing tables, no webhook plumbing, no portal to build.
Charging for an AI app means metering every model call, selling credits, gating premium features, and reconciling all of it with Stripe. That's a database, a webhook handler, a customer portal — and a week you don't have.
PayKit gives you one function — meter() — plus a <Paywall> and a hosted billing API. You call meter() before each AI call; PayKit deducts a credit, tells you if the user is out, and handles buy-more. Subscriptions and credit packs settle through Stripe Checkout and land back in the ledger automatically.
i
PayKit does not run your AI model. Throughout these docs image_gen is just an example event name — your label for one billable action. Name it whatever your app does: chat_message, transcription,render. PayKit only counts and bills.
Getting started
Quickstart
Pick the path that matches your stack. All three hit the same API.
1 · No-code — any website, 30 seconds
Paste one line. Renders a live credits meter + Buy button and wires every data-paykit-* element. Works on Webflow, Wix, WordPress, plain HTML.
PayKit reads the key from (in order): the x-paykit-key header, a key field in the JSON body, or a ?key= query param. No key → the demo project. An unknown key → 401.
Concepts
Metering & credits
Deduction is atomic and stops at zero — a user can't go negative. When credits run out, meter() returns blocked: true (REST: 402) and your code decides what to do. This is the safe default: you never give away unpaid usage.
ts
const { blocked } = await meter("image_gen")
if (blocked) return openBuyCredits() // don't run the model
runYourModel()
Per-call pricing
Pass a cost to charge different amounts per action:
ts
await meter("hd_upscale", 4) // costs 4 credits
Secure metering
By default /meter accepts the publishable key (handy for client/embed use). FlipsecureMetering on a project (PATCH /projects) to require the secret key, so only your server can spend credits. Recommended once you're past the demo.
Concepts
Billing & Stripe
text
User clicks Buy ──▶ POST /checkout ──▶ Stripe Checkout ──▶ payment
│
ledger updated ◀── POST /webhook ◀── checkout.session.completed
(credits granted / plan = pro)
Product
Detail
credits
One-time $9 for a 100-credit pack.
pro
$19/mo subscription. Cancelling reverts the user to free.
The userId rides along in Stripe metadata, so the webhook credits the exact account. For local testing: stripe listen --forward-to localhost:3000/api/v1/webhook.
Reference
React SDK
<PayKitProvider userId>
Wrap your app (or the authed part). Loads the account on mount and exposes the context. The one required prop is userId — your stable id for the current user.
Stripe's endpoint — you don't call this. Point a Stripe webhook at it and set STRIPE_WEBHOOK_SECRET. It verifies the signature, then grants credits / sets the plan on checkout.session.completed and reverts to free on customer.subscription.deleted.
GET/analytics · /accounts
Dashboard data, scoped by key. /analytics returns usage series + top events + stats { total, pro, mrr, creditsOutstanding } (MRR = pro × $19). /accounts returns the account list + stats.
GET · POST · PATCH/projects
Manage tenants (Clerk-authed for create/list). POST { name? } creates a project and returns its keys; PATCH { secureMetering, key: sk_… } toggles secure metering.
Operate
Self-hosting
PayKit runs with zero config (in-memory store + simulate buttons). Add env vars to go real:
Variable
Required for
Notes
DATABASE_URL
persistence
postgres://… (e.g. Neon). Tables auto-create & migrate. Without it → in-memory.
STRIPE_SECRET_KEY
checkout + webhook
sk_test_… / sk_live_…
STRIPE_WEBHOOK_SECRET
webhook
from stripe listen or your dashboard endpoint.
NEXT_PUBLIC_BASE_URL
redirect URLs
your public origin. Falls back to the request origin.
CLERK_*
multi-tenant auth
optional — without it, ownership falls back to a shared demo owner.