golemkit

AI & billing

Payments

Users buy credit packs (one-time payments), defined in src/modules/billing/plans.ts. Payments are provider-agnostic behind the PaymentProvider interface — Stripe (global) and iyzico (Turkey) ship in the box, selected with one env var:

PAYMENT_PROVIDER="stripe"   # or "iyzico"

Billing stays disabled (UI shows a notice, API returns 501) until the chosen provider's keys are set — the app never breaks for lack of a payment account.

Stripe

  1. Set STRIPE_SECRET_KEY (test keys work).
  2. Create a webhook endpoint for checkout.session.completed pointing at /api/stripe/webhook, and set STRIPE_WEBHOOK_SECRET. Locally, stripe listen --forward-to localhost:3000/api/stripe/webhook prints one.

Flow: checkout → hosted payment page → webhook verifies the signature and grants credits.

iyzico

  1. Set IYZICO_API_KEY and IYZICO_SECRET_KEY (sandbox keys from sandbox-merchant.iyzipay.com; IYZICO_URI defaults to the sandbox API).
  2. No webhook setup needed — iyzico's Checkout Form POSTs a token back to /api/iyzico/callback, which retrieves and verifies the payment.

Subscriptions — the credit hybrid

Besides one-time packs, subscriptionPlan in src/modules/billing/plans.tsdefines a monthly plan that auto-grants credits every billing cycle. Stripe-only: checkout uses subscription mode with a dynamically created recurring price (no dashboard products needed), the plan's metadata is snapshotted onto every invoice, and the webhook grants monthlyCredits on invoice.paid— add that event to your webhook endpoint. The invoice id is the idempotency reference, so Stripe's retries can't double-grant.

Why double-grants can't happen

Both providers grant credits through grantCredits()with the provider's payment reference. That reference has a unique constraint in the ledger, so retried webhooks and refreshed callbacks are database-level no-ops.

Adding another provider

Implement the PaymentProvider interface in src/modules/billing/providers/ (one method: createCheckoutUrl), add its completion route, and register it in providers/index.ts. Nothing else in the app changes.