TrademarkTrademark
Ctrl k
Search docs...
Composition

API products

Sell an API with keys, usage, metered billing, REST and an MCP server.
The api surface is everything around an API that is not the API itself: keys, quotas, a usage ledger, metered billing, a developer console, docs, and an MCP server so agents can call it as tools. You write the operations; the surface serves them. It requires the billing slice, which brings orgs and auth with it. Keys belong to an organization and only its admins mint or revoke them; the organization's plan decides its allowance, and its usage is invoiced on the same Stripe subscription as the plan. An operation is defined once in src/resources/api.config.ts: and is then served as:
  • POST /api/v1/links/preview — REST, with the cost of the call in x-units.
  • /api/mcp — the MCP tool preview_link (Streamable HTTP, stateless).
  • /api/openapi.json — an OpenAPI 3.1 path.
  • /developers/reference — a reference entry and the pricing table.
The four cannot disagree about arguments or price, because they all read the same object. Arguments are validated against input before the handler runs, and unknown arguments are refused rather than ignored. Keys look like <keyPrefix>_sk_… and are stored only as a SHA-256 hash; the console shows a key once, when it is created. Calls send it as Authorization: Bearer … (or x-api-key for clients that can set only one header). The developer console at /developers lists keys, revokes them, and shows this period's usage and the invoice so far. Prices live in src/resources/plans.json, next to the plans they belong to. A plan sells API usage when it has a monthly metered price on the product's meter (meterEvent in api.config.ts) with graduated tiers: the free first tier is the allowance, the next tier is the overage price. npm run stripe:sync creates the meter and the price, checkout puts the price on the plan's subscription, and the API reads the same entry for its quota and the reference page's pricing table, so the three cannot disagree. An organization with no subscription (or on a plan without such a price) gets the Free allowance from api.config.ts, with a hard stop. Every billable call appends a row to the usage ledger with its units frozen at call time. The rules live in src/lib/api/metering.ts and follow Chirio's: a hard stop on Free, metered overage on paid plans (a paying customer is never refused mid-month), and an internal billing mode for first-party tenants that are metered but never invoiced. A subscription on a yearly price carries no monthly meter, so it stops at the allowance too. Usage reaches Stripe from the ledger, not from the request: schedule /api/cron/report-usage hourly with CRON_SECRET. It reports each row through the billing slice as a meter event keyed by the row id, so a re-run never bills twice. Without STRIPE_SECRET_KEY the report is mocked and the loop still runs end to end. A handler that throws is not billed. Idempotency-Key on an operation marked idempotent returns the first result to a repeat and bills it once. With no environment at all the surface runs on an in-process store: create a key in the console and call your API straight away. The store forgets on restart and is per server instance, so it is for trying the surface only. For real use, configure Supabase and run supabase/api.sql after billing.sql. node scripts/smoke-api.mjs <url> <cron-secret> checks a running app end to end: key, REST, MCP, ledger, usage report and revocation. An app that already has API keys and its own billing (credits, say) can serve the same REST and MCP transports without the ledger: copy src/lib/api/{schema,operation,product,transport,mcp,rest,openapi}.ts and pass createRestRoute and createMcpRoute a Runtime of its own. That is how the Frametic screenshot API is built.
Last updated October 17, 2026
Edit this page on GitHub