Pricing & subscriptions as code, PSP-agnostic.
Author your pricing in a Planfile, plan/apply it to any payment
service provider, then check entitlements, take usage and handle webhooks at runtime.
# the moneta CLI (single static binary) + the runtime SDK
npm i -g monetakit # or: curl -fsSL monetakit.dev/install | sh
npm i @monetakit/sdk# Planfile — a price with a `recurring` block is a subscription; without one, one-time.
feature "sso" { type = "boolean" }
feature "seats" { type = "seat" }
product "pro" {
name = "Professional"
managed_by = "self"
entitlements = {
sso = true
seats = { included = 5, overage = 800 } # 800 = per extra seat (smallest unit)
}
price "monthly" {
amount = 2000
currency = "usd"
recurring = { interval = "month" }
}
price "annual" {
amount = 20000
currency = "usd"
recurring = { interval = "year" }
}
}
routing { usd = "stripe" } # one catalog, fanned out by currency -> PSP
provider "stripe" { mode = "test" }moneta plan # diff your Planfile against each PSP's live state (--json for tooling)
moneta apply # sync (immutable price chains + grandfather migrations; archive, never delete)
moneta import # import an existing PSP catalog into a Planfile
moneta usage --customer cus_x --feature apiCalls --qty 5 # report metered usage
moneta check --product pro --feature seats --usage seats=5 # evaluate an entitlement against the lock (--json)
moneta drift # out-of-band catalog-edit timeline (from the webhook drift monitor)- Author + sync (Stripe). Products; subscription / one-time / metered prices (per-unit,
fractional, and graduated / volume tiers); features (
boolean/seat/meterwithsum|count|last); entitlements — an entitlementoverageauto-compiles to a metered overage price; currency → PSP routing.plan/applywith immutable price version chains + existing- subscriber grandfather migrations, archive-never-delete (prevent-destroy), resolved price ids written back to the lock. - Money rigor. The 139 Stripe-supported currencies × ISO 4217 decimals; compile-time amount
validation (unknown currency, over-8-digits,
amount/amount_decimalexclusivity, ≤12 dp); human-readable amounts incompile/planthat catch the zero-decimal trap (amount = 100, currency = "twd"shows as1.00 TWD, not NT$100). - Runtime SDK —
@monetakit/sdk, TypeScript, zero-dependency and edge-native (Web Crypto +fetch), so the same code runs on Cloudflare Workers / Node / Bun / Deno, under Next.js / Hono / TanStack Start:check(catalog, customer, feature)— the entitlement gate (reference impl; other languages validated by shared conformance vectors).createCheckout(...),parseWebhook(...)(signature-verified),resolvePriceId(...).
- React bindings —
@monetakit/react:<Gate feature="…">,useEntitlement,<PricingTable>, with feature names typed to your Planfile viamoneta codegen(a mistyped feature is a compile error). - Self-managed recurring engine (
planfiled) — run subscriptions on a pure gateway instead of Stripe Billing (skip its ~0.5–0.8% fee): a durable queue claims each due period, rates it (per-unit / graduated / volume / metered usage), charges the saved card off-session, and records it in an idempotent ledger. Backends: SQLite, Postgres, and Cloudflare D1 behind one conformance suite (Postgres usesFOR UPDATE SKIP LOCKED; SQLite/D1 an atomicUPDATE … RETURNING— writes serialize). - Drift detection. Out-of-band edits in Stripe are caught in real time from webhooks
(Terraform can only poll), classified
display(tolerable) vsstructural— with the count of active subscriptions affected as the major-impact indicator — appended to a versioned ledger, shown bymoneta drift. - Versioned output contract.
moneta plan --jsonand the drift ledger conform toschema/plan.schema.json/schema/drift.schema.json(formatVersion, additive-only) — like Terraform's plan JSON, for CI / dashboards / other-language tooling.
// runtime, self-hosted at the edge (Hono / Workers shown; Next.js & TanStack use the same calls)
import { check, createCheckout, parseWebhookRequest } from "@monetakit/sdk";
import catalog from "./planfile.lock.json";
app.post("/webhooks/stripe", async (c) => {
const event = await parseWebhookRequest(c.req.raw, c.env.STRIPE_WEBHOOK_SECRET); // verifies + normalizes
// event.priceKey === "product:pro:price:monthly" (from the price's pac.key metadata) -> upsert your store
});
app.get("/api/export", (c) => {
if (!check(catalog, { product: userPlan }, "sso").allowed) return c.json({ error: "upgrade" }, 402);
// ...
});The authoring format is HCL (declarative → parsed, not evaluated), so the toolchain spine is Go (native HCL, single static binary, Terraform-style). The compiled Planfile (a JSON IR/lock) is the neutral contract; everything downstream — the runtime SDK, per-language checks — derives from it.
Planfile ──(Go, the only HCL parser)──▶ compiled lock (JSON) ──▶ runtime SDK & tooling (any language)
| Layer | Language |
|---|---|
moneta CLI + stripe adapter, planfile compiler, adapterkit, core; the planfiled engine (rating / engine / store / scheduler) |
Go |
runtime SDK (check / createCheckout / parseWebhook) + @monetakit/react |
TypeScript (reference; other langs via conformance) |
schema/ (IR, capabilities, plan, drift JSON Schema) |
neutral, single source of truth |
cmd/moneta/ CLI: compile / plan / apply / import / usage / drift
cmd/webhookd/ test-mode webhook receiver + real-time drift monitor
planfile/ the Planfile format: IR, HCL compile/decompile, validate, currency/amount rules
adapterkit/ PaymentProvider + optional UsageReporter / WebhookParser / DriftDetector
adapters/stripe/ tier: core (stripe-go) — Read/Apply, meters, ParseWebhook, DetectDrift
core/ diff/plan, plan --json + drift ledger, id-writeback, migration ledger, affected-subs
schema/ ir / capabilities / plan / drift (versioned neutral contract)
rating/ self-managed rater: per-unit / graduated / volume tiers + usage aggregation
engine/ period processor: rate + off-session charge for one subscription period
store/ durable queue + idempotent ledger — SQLite & Postgres (one conformance suite)
scheduler/ the engine loop: claim due -> ProcessPeriod -> record
cmd/planfiled/ the self-managed recurring engine daemon (SQLite/Postgres + HTTP + timer)
packages/sdk/ @monetakit/sdk — check / checkout / webhook / catalog (+ conformance vectors)
packages/react/ @monetakit/react — Gate / useEntitlement / PricingTable (types via moneta codegen)
examples/ reference/Planfile, hono-cloudflare/, vite-react/ (compiled to planfile.lock.json)
docs/ planfile/ (format), monetakit/ (product + output contract)
website/ planfile.dev + monetakit.dev docs sites (Kura; pull docs/ via content.sources)
- core — bundled in the CLI, the reference, highest bar. Today: Stripe (via
stripe-go). - official — first-party, installed on demand. Planned: PayPal.
- community — third-party
monetakit-adapter-*, built against@monetakit/adapter-kitand validated by the conformance suite.
- Dunning & invoicing — retry failed ledger charges with backoff; emit invoices from the ledger.
- PayPal adapter (Rule-of-Three for the
adapterkitinterface). - More runtime targets — Next.js / TanStack Start examples (the SDK already runs there).
Docs: monetakit.dev (product) · planfile.dev
(format) — Kura sites in website/, sourced from docs/.