Commercial layer
May 6, 2026 · View on GitHub
Epistemic framing (pointer only): epistemic-contract.md. Adoption and verdict semantics: adoption-epistemics.md.
This document is the narrative SSOT for the thin commercial layer (website, billing, API keys, CLI preflight). It does not redefine CLI verification semantics—see verification-product.md and agentskeptic.md.
Related (integrator, not duplicated here): first-run-integration.md — run verification against your own SQL database; rendered on the site as /integrate.
HTTP activation (correlation IDs, Problem Details errors, OpenAPI coverage): commercial-http-activation.md.
Commercial boundary
The published npm CLI path for licensed stateless checks via check (and positional compatibility invocation) and quick uses a valid API key and a successful POST /api/v1/usage/reserve. Starter includes a finite free monthly allowance for licensed stateless checks (see config/commercial-plans.json). Stateful enforce (baseline, drift check, acceptance over time) requires a paid plan with an active Individual, Team, Business, or Enterprise subscription (Stripe trialing counts). The default OSS build runs stateless contract checks / quick without a license server for single-run outcomes only. Paid plans add metered overage after the included amount (Stripe subscription has a base Price + metered overage Price; see scripts/stripe-bootstrap.mjs). Full matrix: Free vs paid boundary below.
In-process createDecisionGate in your application evaluates read-only SQL and does not call the reserve API; metering applies to the CLI entry points that perform license preflight.
Evaluation path
Run the mechanical first-run path on the canonical site at /integrate (clone, build, bundled demo, then crossing on your prepared SQLite or Postgres). When you need Stripe-backed metering, API keys, and plan caps, use /pricing on the same site and keep this repository’s commercial SSOT (docs/commercial.md) as the normative contract for entitlements.
Operator funnel metrics (North Star): funnel-observability.md — acquisition and integrate impressions, anonymous CLI activation (verify_started / verify_outcome via product_activation_*_beacon on telemetry Postgres), and licensed CLI completion beacons on core (funnel_event / verify_outcome_beacon). Storage split: telemetry-storage.md. Stage-separated rolling conversion metric ids live only in growth-metrics.md: CrossSurface_ConversionRate_AcquisitionToIntegrate_Rolling7dUtc, CrossSurface_ConversionRate_IntegrateToVerifyOutcome_Rolling7dUtc, CrossSurface_ConversionRate_QualifiedIntegrateToVerifyOutcome_Rolling7dUtc (integrate→outcome with workload_class = non_bundled numerator—see Qualification proxy (operator)), CrossSurface_ConversionRate_QualifiedIntegrateToIntegratorScopedVerifyOutcome_Rolling7dUtc (same denominator; numerator also requires workflow_lineage = integrator_scoped on schema v3 activation rows—see growth-metrics.md §CrossSurface_ConversionRate_QualifiedIntegrateToIntegratorScopedVerifyOutcome_Rolling7dUtc), and the existing compressed CrossSurface_ConversionRate_AcquisitionToVerifyOutcome_Rolling7dUtc. Interpretation (user vs telemetry capture) is normative under User outcome vs telemetry capture (operator). CLI activation POST reachability (403/400/204 behaviors and split-origin guidance) is normative only under Activation reachability (operator) in funnel-observability.md—not duplicated here.
Approved product scope (v1)
Normative numbers and prices are only in config/commercial-plans.json (schemaVersion: 2). Do not duplicate tier tables here—this file explains behavior.
Implementation (v2):
- Included + overage: Per-plan monthly included verification count and per-verification overage rate (micro‑USD in JSON) apply to paid self-serve tiers.
POST /api/v1/usage/reserveallows usage past included whenallowOverageis true; Starter has a hard cap (no overage). Enterprise is unlimited / contract. - Stripe: Checkout creates two line items (licensed base + metered overage).
user.stripe_price_idstores the non‑metered base Price id (flatPriceIdFromSubscriptionin code). Overage quantities are reported to Stripe on a schedule (seePOST /api/internal/usage/overage-reconcile).
Enterprise is sales-assisted only (mailto + operator SQL). It is outside the self-serve non-negotiable outcome and outside the binary solved verdict for the commercial funnel.
Numeric limits (SSOT)
Source of truth: config/commercial-plans.json — included amounts, list prices, annual prepay cents, overage microusd/verification, and Stripe env key names (not secrets).
Free vs paid boundary (normative v1)
Single matrix for what the default OSS artifact vs published commercial npm vs website Starter account allow. “Paid” here means an active self-serve subscription on Individual, Team, or Business (or operator-granted Enterprise) and a successful POST /api/v1/usage/reserve where applicable—not merely having an API key on Starter.
| Capability | OSS build (WF_BUILD_PROFILE=oss) | Commercial npm + subscription + reserve | Starter account (no paid subscription) |
|---|---|---|---|
Contract verify / quick without API key | Yes | No (requires key + reserve + entitlement) | N/A (use OSS or subscribe) |
Stateful enforce baseline/drift/accept | No | Yes (reserve intent=enforce) | N/A |
| Licensed monthly quota consumption | No | Yes, per API key; included then overage on paid | Yes, up to Starter included cap per key (includedMonthly in JSON; no overage) |
Why this shape: OSS stays useful for adoption and local experimentation (including generating lock artifacts). Subscription-backed reliance for the published npm path—licensed verify, compare against an existing lock in CI, and enforce—is gated by the license server and Stripe-backed entitlement. Normative CLI split: docs/commercial-enforce-gate-normative.md; CI recipes: docs/ci-enforcement.md.
Outcome Certificate vs billing: The public Outcome Certificate JSON never contains Stripe, plan, or quota fields. Subscription and reserve gate whether verification is allowed to run; see outcome-certificate-normative.md and outcome-certificate-integrator.md.
Programmatic verification vs licensed CLI
In-process createDecisionGate (library embed in your application) evaluates read-only SQL against buffered structured events and does not call POST /api/v1/usage/reserve. Licensed metering applies to the published npm CLI entry points that perform license preflight before stateless contract checks (check / positional compatibility), quick with lock flags, and enforce.
Packaging and CLI build profiles
| Artifact | WF_BUILD_PROFILE | Behavior |
|---|---|---|
| OSS / this repo CI | oss (default) | No license preflight; stateless contract checks via check (and positional compatibility) without API key; enforce unavailable — docs/commercial-enforce-gate-normative.md |
| Published npm tarball | commercial | Requires AGENTSKEPTIC_API_KEY (legacy WORKFLOW_VERIFIER_API_KEY accepted) + successful preflight for contract batch, quick verify, and enforce |
Codegen: node scripts/write-commercial-build-flags.mjs writes src/generated/commercialBuildFlags.ts (gitignored) before tsc. npm run build passes --oss so the default artifact stays OSS even if WF_BUILD_PROFILE is set in the shell; npm run build:commercial invokes the script with --commercial and requires COMMERCIAL_LICENSE_API_BASE_URL.
After tsc, the embedded license API origin is LICENSE_API_BASE_URL in dist/generated/commercialBuildFlags.js (not necessarily duplicated inside dist/cli.js). CI sets this from the COMMERCIAL_LICENSE_API_BASE_URL repository variable when .github/workflows/release.yml runs semantic-release and the commercial prepublishOnly build.
Forks: build with oss to omit the gate.
HTTP — POST /api/v1/usage/reserve
- Auth:
Authorization: Bearer <api_key> - Body:
{"run_id": string, "issued_at": ISO8601, "intent"?: "verify"|"enforce"}; reject if|now - issued_at| > 300seconds. - 200:
{"allowed":true,"plan","limit","used","included_monthly","overage_count"}(totalusedmay exceedlimiton paid overage) - 401: invalid/revoked key
- 403:
QUOTA_EXCEEDED,ENFORCEMENT_REQUIRES_PAID_PLAN,SUBSCRIPTION_INACTIVE,BILLING_PRICE_UNMAPPED(paid plans with a setuser.stripe_price_idonly), or other entitlement/deny bodies; may includeupgrade_url - 400: bad request
- 503: server error
Emergency: RESERVE_EMERGENCY_ALLOW=1 — valid keys on individual/team/business/enterprise bypass the inactive subscription check for verify and enforce. Starter enforce remains denied (paid-only). Starter verify is not subscription-gated (free included quota). BILLING_PRICE_UNMAPPED is never bypassed for paid subscriptions when a price id is set. Quota and idempotency unchanged (still enforced).
BILLING_PRICE_UNMAPPED: returned for paid plan users when user.stripe_price_id is set and the deployment’s base STRIPE_PRICE_* env values do not recognize that Price id. Remediation: align monthly, yearly, and overage STRIPE_* envs with Stripe, redeploy, or contact the operator.
HTTP — GET /api/v1/commercial/plans
- Auth: none
- 200:
{"schemaVersion", "plans"}with public fields only (no Stripe price env key names). Same shapes as the OpenAPICommercialPlansResponsecomponent.
HTTP — GET /api/v1/usage/current
- Auth:
Authorization: Bearer <api_key> - Scope: requires
meter - 200:
UsageCurrentV1payload (schema_version=1) with month bounds, pooled usage, included quota, overage count,quota_state,allowed_next, andestimated_overage_usd - 403: insufficient scope
- 503: plans unavailable or server error
Quota semantics (normative)
- Quota policy is account-pooled per month (not per-key caps).
- Reserve decisions are computed from transaction-locked state + pure quota policy rules.
GET /api/v1/usage/current, account usage UI, CLIagentskeptic usage, and overage reconcile all derive from the same pooled semantics.- If a user plan is missing in plans config:
- reserve returns
403 SUBSCRIPTION_INACTIVE(Invalid plan configuration.) - usage/current returns
503 PLANS_UNAVAILABLE - overage reconcile skips row and logs
overage_reconcile_invalid_plan
- reserve returns
Subscription state, Stripe webhooks, and account API
Normative detail for billing sync, post-checkout UX, and deletion semantics lives here (do not duplicate in other docs—link to this section).
Stripe → database
- Webhooks:
checkout.session.completed,customer.subscription.updated,customer.subscription.deleted. Operator env:STRIPE_SECRET_KEY,STRIPE_WEBHOOK_SECRET,STRIPE_PRICE_*(monthly + yearly base),STRIPE_OVERAGE_*(metered),CRON_SECRET(internal overage report route). user.stripe_price_id: nullable; stores the licensed (non‑metered) base recurring Stripe Price id (seeflatPriceIdFromSubscription). Used to computepriceMappingon the account API.- Tier (
user.plan): derived from that Price id via env-backed mapping inconfig/commercial-plans.json(STRIPE_PRICE_*monthly and yearly variants). The metered overage line item is not used foruser.plan. - Unknown Price id:
planis left unchanged;stripe_price_idstill records the id; logsstripe_price_unmapped; account showspriceMapping: unmappedand entitlement copy includes an operator-contact suffix.POST /api/v1/usage/reservereturns403withBILLING_PRICE_UNMAPPED(no quota consumed) until mapping is fixed.
Customer Billing Portal and Checkout customer reuse
Why two surfaces: Stripe Checkout (via POST /api/checkout) is the first-purchase path. Stripe Customer Billing Portal (via POST /api/account/billing-portal) is the ongoing self-serve path for payment methods, invoices, cancellation, and plan/price changes as enabled in the Stripe Dashboard for that Customer. They are separate guarantees: Checkout can succeed without Portal ever being opened; Portal requires a persisted user.stripe_customer_id.
Checkout: Session params are built in website/src/lib/stripeCheckoutSessionParams.ts. If stripe_customer_id is already on the user row, Checkout passes customer (and does not send customer_email); otherwise customer_email is used for the first Stripe Customer creation path.
Billing Portal session — POST /api/account/billing-portal (session cookie, same auth model as other account routes):
| Status | Body |
|---|---|
| 200 | {"url":"<string>"} — redirect browser to url |
| 401 | {"error":"Unauthorized"} |
| 404 | {"error":"STRIPE_CUSTOMER_MISSING","message":"…"} — no stripe_customer_id yet (complete Checkout once) |
| 500 | {"error":"Internal Server Error"} — Stripe misconfiguration or API failure; server logs JSON line {"kind":"billing_portal_session_failed",...} |
Return URL: {NEXT_PUBLIC_APP_URL}/account (trailing slash stripped).
Account UI: Manage billing is rendered only when GET /api/account/commercial-state (and server-rendered initial state) include hasStripeCustomer: true (non-empty trimmed stripe_customer_id).
GET /api/account/commercial-state (authenticated): JSON includes existing plan and billing fields plus monthlyQuota: yearMonth (UTC YYYY-MM), keys[] with per–API-key used and limit (use null for unlimited enterprise included monthly), distinctReserveUtcDaysThisMonth (count of distinct UTC calendar dates with a reserve_allowed row this month — account activity gauge only), and worstUrgency (ok | notice | warning | at_cap) from usage vs plan thresholds. Operator rolling retention KPIs live in docs/growth-metrics.md; do not label the month gauge as that retention KPI in UI copy.
Operator — Stripe Dashboard: Enable the Customer billing portal; link the same Products/Prices used for self-serve Checkout so customers can switch plans without leaving Stripe’s UI. Misconfiguration surfaces as 500 on POST /api/account/billing-portal until fixed.
customer.subscription.deleted
Single row semantics (match subscription + customer when possible; else fall back to customer id):
subscription_status→inactiveplan→starterstripe_subscription_idandstripe_price_id→nullstripe_customer_idunchanged (reuse for a future checkout)
HTTP — GET /api/account/commercial-state (session cookie)
- Auth: signed-in website user (NextAuth session).
- Query: optional
expectedPlan=individual|team|businessonly; any other value → 400. - 200 body (always):
plan,subscriptionStatus,priceMapping,entitlementSummary,checkoutActivationReady,hasStripeCustomer. checkoutActivationReady:trueonly when the query includes a validexpectedPlanand the user row satisfiesplan === expectedPlan,subscriptionStatus === active,priceMapping === mapped, and licensedverifywould proceed perwebsite/src/lib/commercialEntitlement.ts(no emergency flag). Used by/accountafter Checkout success polling. Trialing in Stripe maps toactivein the DB (website/src/lib/stripeSubscriptionStatus.ts).
OpenAPI: this route is not part of schemas/openapi-commercial-v1.yaml.
Operator verification
From the repo root, npm run validate-commercial requires DATABASE_URL and TELEMETRY_DATABASE_URL, runs website/scripts/db-migrate.mjs and website/scripts/db-migrate-telemetry.mjs, then full website Vitest (including funnel DB tests), then scripts/pack-smoke-commercial.mjs and npm run build to restore OSS dist/.
Machine contracts (OpenAPI)
- Normative file (repo):
schemas/openapi-commercial-v1.yaml - Deployed URL (static):
/openapi-commercial-v1.yamlon the app origin. The file is generated intowebsite/public/duringwebsiteprebuildbynpm run sync:public-product-anchors(which runsemit-primary-marketing) from the repo root, then served as a static asset. The committed copy underschemas/is the canonical spec for review; the public copy may use the effective deployment origin forserversand the self-URL. - Scope: this bundle describes hosted commercial, reporting, enforcement, and related HTTP APIs. The default runtime truth-check integration is
agentskeptic check/AgentSkeptic.check(Outcome Certificate +truth_check_verdictstderr); start atdocs/first-truth-check.md, thendocs/integrate.mdfor depth — not/api/verifydemo routes.
Public anchors and OpenAPI source
The editable OpenAPI “header” and distribution tokens live in schemas/openapi-commercial-v1.in.yaml. schemas/openapi-commercial-v1.yaml is generated — do not hand-edit. Rationale (single source for URLs, valid OAS layout, no placeholder hosts): docs/public-distribution.md.
/integrate and integrator documentation
- Operator signal (integrate → qualified
verify_started, rolling 7d UTC): see metric idCrossSurface_ConversionRate_QualifiedIntegrateToVerifyStarted_Rolling7dUtcingrowth-metrics.md—not proof of Decision-ready ProductionComplete (artifact bar inadoption-epistemics.md). - SSOT prose and commands remain
docs/first-run-integration.mdanddocs/partner-quickstart-commands.md. /integrateroute:website/src/app/integrate/page.tsxrenders pack-led copy fromconfig/marketing.json(integratePage: one command block, requirements, proof line, GitHub deep links) pluswebsite/src/content/siteMetadata.tsfor theh1/ description. It does not importintegratorDocsEmbeddedat build or runtime.- Build-embedded strings:
website/src/generated/integratorDocsEmbedded.tsis still produced bynode scripts/sync-integrator-docs-embedded.mjsduringwebsiteprebuildso CI parity tests (website/__tests__/integratorDocsEmbedded.parity.test.ts,website/__tests__/integrate-embedded.no-filesystem.test.ts) can assert the generated blobs trackdocs/without reading the monorepodocs/tree from the deployed serverless bundle for/integrate.
Concurrency: Monthly counter row must be locked with SELECT … FOR UPDATE in the same transaction as idempotent (api_key_id, run_id) insert.
CLI environment
| Variable | Purpose |
|---|---|
AGENTSKEPTIC_API_KEY | Plaintext API key (commercial build); legacy WORKFLOW_VERIFIER_API_KEY still read |
AGENTSKEPTIC_RUN_ID | Optional idempotency key (default: random UUID); legacy WORKFLOW_VERIFIER_RUN_ID still read |
Retries on 429/502/503/504: 250ms, 750ms, 2250ms (3 attempts), then exit 3 LICENSE_USAGE_UNAVAILABLE.
Operational codes include: LICENSE_KEY_MISSING, LICENSE_DENIED, LICENSE_USAGE_UNAVAILABLE, ENFORCEMENT_REQUIRES_PAID_PLAN, ENFORCE_REQUIRES_COMMERCIAL_BUILD (see src/cliOperationalCodes.ts). The license preflight may still map a legacy VERIFICATION_REQUIRES_SUBSCRIPTION HTTP code for older deployments.
Auth email (production vs E2E)
- Production: Resend SDK only (
resendpackage). - E2E only: When
E2E_COMMERCIAL_FUNNEL=1, magic link email is sent via Nodemailer SMTP to Mailpit atsmtp://127.0.0.1:1025. Never setE2E_COMMERCIAL_FUNNELin production deploys. - Rate limits: Magic link send throttling (caps, reservation algorithm, deny logs) is normative in website-magic-link-rate-limit.md — do not duplicate numeric caps here.
Legal effective date
config/legal-metadata.json is the sole source for effectiveDate and termsVersion (no env overrides).
Database migrations
From website/ with DATABASE_URL and TELEMETRY_DATABASE_URL set:
npm run db:migrate
npm run db:migrate:telemetry
Core migrations live in website/drizzle/ (e.g. 0000_initial.sql). Telemetry migrations live in website/drizzle-telemetry/.
Validation matrix (Layer 2)
Services (see docker-compose.commercial-e2e.yml):
- Postgres 16 — app
DATABASE_URLplus telemetryTELEMETRY_DATABASE_URL. For Supabase on Vercel, usesslmode=require(or rely on helpers inwebsite/src/db/ensureSslModeRequire.ts): thepostgres.jsclient usesensureSslModeRequire();npm run db:migrate(drizzle-kit under the hood) usesnode-pg, which currently treats baresslmode=requirelikeverify-fulland can throwSELF_SIGNED_CERT_IN_CHAIN—sodrizzle.config.tsusesensureDatabaseUrlForNodePgDriver(), which addsuselibpqcompat=trueas required by thepg/pg-connection-stringmigration warning. - Mailpit — SMTP
127.0.0.1:1025, UI/API8025
Stripe CLI: stripe listen --forward-to <BASE_URL>/api/webhooks/stripe — use the printed whsec_… as STRIPE_WEBHOOK_SECRET for that process.
Mailpit messages API: GET http://127.0.0.1:8025/api/v1/messages (see Mailpit docs for stable JSON shape).
Staging checklist (self-serve billing — binary “solved”)
Run once per environment with test-mode Stripe keys before promoting:
stripe listenforwarding to/api/webhooks/stripe;STRIPE_WEBHOOK_SECRETmatches the listener.- Sign in,
POST /api/checkoutfor a self-serve plan → complete Checkout → confirm webhook updatesuser(plan,subscription_status,stripe_customer_id,stripe_subscription_id,stripe_price_id). /account:hasStripeCustomertrue; Manage billing opens Portal; return lands on/account.- Second Checkout while logged in: Stripe Dashboard shows one Customer for that test user (reuse via
customeron Checkout). POST /api/v1/usage/reservewith API key returns 200 when subscription active and price mapped.- Negative: set
stripe_price_idto an unknown Price id in DB (test only) → reserve returns403BILLING_PRICE_UNMAPPED; restore row.
Verdict: Solved only if steps 1–6 pass; otherwise not solved.
Enterprise operator runbook
-- Example: grant enterprise (exact table/column names follow Drizzle schema in website)
UPDATE "user" SET plan = 'enterprise', subscription_status = 'active' WHERE email = 'customer@example.com';
API key storage
API keys are verified with Node crypto.scrypt (parameters fixed in website code and reviewed with security in mind). Store salt + hash only; show plaintext once at creation.
Scoped API keys (api_key_v2) — normative
- Canonical table for active key management is
api_key_v2(multi-key, scoped, labeled, rotatable). - Legacy
api_keyremains read-only during migration only. - Allowed scopes:
read,meter,report,admin. - Validation rules:
- scopes must be non-empty and contain only allowed values,
- label must match
^[A-Za-z0-9 _./:-]+$and length1..64, - label uniqueness is enforced per user among active keys (case-insensitive),
expires_atis optional; when set, must be at least 5 minutes in the future.
- Lifecycle states:
active,revoked,disabled; expiry is evaluated fromexpires_at.
- Rotation contract:
- predecessor is row-locked,
- successor is inserted with copied label/scopes and new secret material,
- predecessor is revoked in the same transaction,
- rollback leaves predecessor active if successor insert fails.
- Route auth scopes (bearer-key routes only):
POST /api/v1/usage/reserverequiresmeter,POST /api/v1/funnel/verify-outcomerequiresreport.
- Account key-management routes are session-authenticated (owner-scoped), not bearer-key-scoped.
- Legacy cutover policy:
- T0 = first production deploy with v2 routes,
- legacy adapter is deleted at T0 + 90 days (hard removal).
Auth failure contract (normative)
401 INVALID_KEY(unknown key / hash mismatch)401 KEY_REVOKED401 KEY_DISABLED401 KEY_EXPIRED403 INSUFFICIENT_SCOPE(includes required scopes in response payload)
last_used_at reliability contract (normative)
last_used_atupdates are non-blocking to avoid coupling authentication to observability writes.- On write failure, server emits
api_key_last_used_write_failedtelemetry. - Repair path retries failed updates asynchronously.
Roadmap (v1.1+)
- (Reserved for future product work.) Metered overage after included quota is live in
config/commercial-plans.jsonand Stripe; buyer-visible rates are built from that file inwebsite/src/lib/commercialNarrative.ts.