Drop-in auth + billing for any StripeΒ +Β WorkOS app, usable by humans and AI agents,
exposed as MCP tools, a REST API, and a CLI, over one storage-agnostic engine.
Billing Tools packages the "get money in" plumbing you'd otherwise rebuild in every SaaS: API-key auth on top of WorkOS, token/credit + subscription billing on top of Stripe, and (because the next wave of customers is autonomous) first-class agent rails: auth.md self-registration and MPP machine payments. Wire it once; serve humans through a browser and agents through headless HTTP with the same engine. Storage is pluggable behind one small adapter (use the built-in WorkOS store, or mirror into your own Postgres).
- Ship tools, not billing plumbing
- Key Features
- Getting Started
- How it works
- Agent auth (auth.md)
- Machine payments (MPP)
- MCP OAuth proxy (dynamic clients)
- CLI
- Configuration
- Roadmap
- Contributing
- License
The next generation of software is agents, and an agent is only as useful as the tools it can call. Tools are the product now.
But you don't go to war naked. Ship a tool into the wild with no auth and no way to get paid, and you're just donating GPU time to strangers. Someone still has to meter every call, hold a balance, bill the workspace, enforce seats, and cut off the freeloaders: the boring, identical, get-it-wrong-once-and-bleed-money layer that every AI product rebuilds from scratch.
Billing Tools is the armor. Bring a Stripe + WorkOS stack and you inherit, out of the box, the exact monetization model the frontier labs run on. The same shape as Anthropic, OpenAI, and xAI (Grok):
- π° A price per token: every tool call costs tokens, metered and deducted automatically.
- π’ Workspaces: the billable account, with one Stripe customer + balance each.
- π₯ Users per workspace: seats, invitations, and roles.
- πͺ Tokens per user: per-seat grants, seat limits, and auto-reload.
So you do the one thing only you can do (build great tools) and monetize them the same afternoon. The get-paid layer is already handled, battle-tested, and agent-ready. π‘οΈ
- π API-key auth: WorkOS organization API keys (
Authorization: Bearer sk_β¦), validated on every request. - βοΈ Passwordless magic-auth: email a 6-digit code, get back a workspace/org + API key. No passwords.
- π€ auth.md agent self-registration: the WorkOS auth.md protocol (RFC 9728 Protected-Resource-Metadata + RFC 8414 Authorization-Server-Metadata +
/agent/identityanonymous or verified-email + the claim ceremony), so agents onboard with no human signup. - β»οΈ RFC 7009 revocation: revoke a key by value; always-200, no token-existence leak.
- π« Optional OAuth-JWT hook: bring your own MCP OAuth proxy via a single adapter method.
- πͺ Usage-metered token wallet: held in the native Stripe customer credit balance (1 token = 1Β’), with idempotent credit/debit.
- π Checkout top-ups: Stripe Checkout that auto-offers cards + AppleΒ Pay / GoogleΒ Pay / Link.
- π¦ Declarative subscription plans: describe plans in code; products/prices are auto-provisioned in Stripe via
lookup_key(immutable-price safe, orphan-cleaning). Zero dashboard clicks. - ποΈ Per-seat / per-cycle token grants + seat limits: included tokens scale with active members.
- π Auto-reload: off-session saved-card recharge when the balance drops below a threshold.
- π§Ύ Invoices + ποΈ Customer Portal: list invoices, and a one-call Stripe Billing Portal URL for self-serve upgrade/downgrade/cancel + card updates.
- π Idempotency on every money move: welcome bonus, checkout credit, and per-cycle grants each carry a stable key, so retries/replays never double-charge.
- π auth.md: the agent onboarding standard, served for you (
/auth.mdnarrative + discovery metadata). - π§ MPP machine payments: Stripe's Machine Payments Protocol: a HTTP 402 +
WWW-Authenticate: Paymentchallenge for pay-per-request (SPT card or crypto/USDC), the payment sibling of auth.md's 401. - π§° MCP server tools:
get_api_key,get_token_balance,buy_tokens,list_plans,get_billing_portal, and more, drop straight into Claude, Cursor, or any MCP client. - π§ Discovery hints: every 401/402 advertises
resource_metadata, so an agent can bootstrap unattended.
- π οΈ MCP:
createMcpTransport({ register, adapter }). - π REST:
createToolListHandler+createToolDispatchHandler(429/401/400/404 mapping built in). - β¨οΈ CLI:
registerBillingCommands(program, β¦)gives youauth,keys,balance,buy,invoices. - πͺ Stripe webhook:
createStripeWebhookHandler()for instant checkout crediting.
- π‘ Event polling: reconcile Stripe and WorkOS via their Events APIs. No webhook endpoints, no signing secrets.
- β±οΈ Any scheduler: run it in-process with
sync.start()(persistent hosts) or as a serverless cron viacreateSyncRoute(). - πͺ Generic DB mirror: shadow WorkOS orgs/users into your own tables (
createMirror), driver-agnostic. - π Dunning hook:
invoice.payment_failedflips the org topast_dueand fires anonPaymentFailedhook (Stripe Smart Retries do the retries).
- β‘ Web-standard handlers: everything returns
RequesttoResponse; mount in Next.js, Hono, Bun, Deno, or Cloudflare Workers. - π§± No framework imports: runtime deps are just
stripe+@workos-inc/node. - ποΈ Bring your own DB: no
pgdependency; you pass aqueryexecutor (any Postgres-compatible driver). - π Pluggable storage adapter: WorkOS-only, or WorkOS + your DB mirror, behind one interface.
- βοΈ No lock-in: runs on any Node host (Railway / Render / Fly / Vercel / self-host).
- π SDK-first: thin-wraps the Stripe & WorkOS SDKs (SDK types, pagination, typed errors, idempotency) so the lib evolves with the platforms instead of drifting.
- π― One memoized client per SDK: lazy, never constructed at import.
- π Secure by default: AES-256-GCM session encryption at rest, SHA-256-hashed claim tokens, verified-domain-only internal-org checks.
- π§ͺ Offline-testable: the auth.md + MPP protocol surfaces are pure
RequesttoResponse, unit-testable without a live account.
- Node 18+
- A Stripe secret key (
STRIPE_SECRET_KEY) - A WorkOS API key + client id (
WORKOS_API_KEY,WORKOS_CLIENT_ID)
npm install @arnaudjnn/billing-tools
# peers (provide the ones you use): @modelcontextprotocol/sdk mcp-handler zodShips compiled dist/, so there's no build step or transpilePackages needed in the consumer.
STRIPE_SECRET_KEY=sk_test_β¦
STRIPE_WEBHOOK_SECRET=whsec_β¦ # only if you mount the webhook
WORKOS_API_KEY=sk_β¦
WORKOS_CLIENT_ID=client_β¦
INTERNAL_ORG_DOMAINS=acme.com # optional: orgs with these verified domains are unmeteredcreateBilling() composes every surface from a single config (one module instance = one shared auth context). You supply the adapter + config; it returns the mounted handlers.
// billing.ts
import { createBilling } from "@arnaudjnn/billing-tools";
import { WorkOSOrgAdapter } from "@arnaudjnn/billing-tools/adapters/workos-org";
export const { mcp, restList, restDispatch, webhook, agentAuth } = createBilling({
adapter: new WorkOSOrgAdapter(), // WorkOS is the source of truth
config: { currency: "usd", baseUrl: process.env.APP_URL! },
agentAuth: { branding: { productName: "Acme" } }, // enables auth.md (optional)
});// app/[transport]/route.ts β export const { GET, POST } = mcp
// app/api/v0/route.ts β export const GET = restList
// app/api/v0/[tool]/route.ts β export const POST = restDispatch
// app/api/stripe/webhook/route.tsβ export const POST = webhookPrefer fine-grained control? Every factory is exported individually (registerBillingTools, createMcpTransport, createToolListHandler, createToolDispatchHandler, createStripeWebhookHandler, createAgentAuth, β¦) so you can build your own composition root.
# List available tools + costs
curl https://your-app.com/api/v0
# Use a key (Bearer sk_β¦)
curl -X POST https://your-app.com/api/v0/get_token_balance \
-H "Authorization: Bearer sk_β¦"Add agent onboarding with createAgentAuth and pay-per-call with createMachinePaymentHandler. See below.
WorkOS is always the source of truth. Your app's orgId is opaque: implement the adapter and everything (auth, metering, Stripe math, all surfaces) works unchanged. Two shipped patterns:
| Pattern | What orgId maps to |
Storage | Use when |
|---|---|---|---|
| A: WorkOS-only | the WorkOS org id | none beyond WorkOS | you don't have (or need) your own DB rows |
| B: WorkOS + DB mirror | your own id (e.g. ws_β¦) |
a workos_org_id column + org.externalId, 1:1 |
you keep local rows and mirror WorkOS into them |
In both, API keys are WorkOS org keys and the Stripe pointer lives on the org. The WorkOSOrgAdapter (@arnaudjnn/billing-tools/adapters/workos-org) implements both; pass a map for Pattern B.
Implements the WorkOS auth.md spec.
import { createAgentAuth } from "@arnaudjnn/billing-tools";
export const agentAuth = createAgentAuth({
adapter, config,
branding: { productName: "Acme", logoUri: "https://acme.com/logo.svg" },
identityTypes: ["anonymous", "verified_email"],
});
// mount: /auth.md, /.well-known/oauth-{protected-resource,authorization-server},
// /agent/identity, /agent/identity/claim, /oauth/token, /oauth/revokeAn agent hits a 401, follows the resource_metadata hint to your metadata, reads /auth.md, then registers (either anonymously for an instant key, or via a verified-email claim ceremony where the user reads back a code) and starts calling tools. All handlers are Request to Response.
Implements Stripe's Machine Payments Protocol.
import { createMachinePaymentHandler } from "@arnaudjnn/billing-tools";
const pay = createMachinePaymentHandler({ methods: ["stripe"], amount: 50, currency: "usd" });
const gate = await pay.requirePayment(request);
if (gate instanceof Response) return gate; // 402 + WWW-Authenticate: Payment challenge
// else settlement succeeded, serve the paid resourceThe 402 challenge + credential parsing ship ready and are offline-testable. Actual settlement (card via shared payment tokens, or crypto/USDC) is injected via a settle function once your Stripe account is enabled for machine payments; until then the gate returns a clean "settlement not enabled" 402 (never a crash). createPaymentMd() serves an agent-facing /payment.md.
MCP clients like Claude Desktop and Claude.ai can't be handed an API key β they
register themselves (RFC 7591) and expect an OAuth 2.1 authorization-code flow.
oauthProxy gives you that on top of WorkOS AuthKit, in one option:
const billing = createBilling({
adapter, config,
agentAuth: { branding: { productName: "Acme" } },
oauthProxy: true, // needs REFRESH_TOKEN_SECRET
});
// app/oauth/authorize/route.ts export const GET = billing.oauth!.authorize
// app/oauth/register/route.ts export const POST = billing.oauth!.register
// app/oauth/callback/route.ts export const GET = billing.oauth!.callback
// app/oauth/token/route.ts export const POST = billing.oauth!.tokenThe user authenticates with AuthKit; the client receives the WorkOS access token
plus an HS256 refresh token that wraps the WorkOS one and is bound to the
client_id that obtained it. PKCE (S256) is enforced whenever the client sends a
challenge, authorization codes are single-use, and the /oauth/token route also
serves the auth.md claim grant so one route covers both flows.
Enabling it also makes discovery honest: authorization_endpoint and
registration_endpoint appear in /.well-known/oauth-authorization-server only
when the proxy is on, and authorization_code/refresh_token join
grant_types_supported. Consumers without the proxy no longer advertise an
/oauth/authorize they don't implement.
REFRESH_TOKEN_SECRET is required and has no fallback. Falling back to
WORKOS_CLIENT_ID β a public identifier β would let anyone who knows it forge a
30-day refresh token. Without the secret the token endpoint returns
server_error rather than signing with something guessable.
import { registerBillingCommands } from "@arnaudjnn/billing-tools";
import { Command } from "commander";
const program = new Command();
registerBillingCommands(program, { configDir: "~/.acme", envPrefix: "ACME", defaultUrl: "https://acme.com" });
// acme auth | keys list|revoke | balance | buy | invoicesBillingConfig (pass to resolveConfig):
| Field | Default | Purpose |
|---|---|---|
baseUrl |
required | Checkout success/cancel + portal return URLs |
currency |
"usd" |
Stripe currency |
freeTokens |
100 |
Welcome credit on first customer creation |
internalDomains |
[] |
Orgs with these verified WorkOS domains are unmetered (see internalDomainsFromEnv) |
- π§Ύ Stripe Tax (
automatic_tax) opt-in - πͺ x402 machine-payment protocol alongside MPP
Issues and PRs welcome. The engine is plain TypeScript compiled with tsc; dist/ is committed (consumers install via Git). See AGENTS.md for the architecture, the adapter interface, the SDK-first doctrine, and the release flow.
MIT Β© Arnaud Jeannin