License Matrix — hosting mode × scope
Every AxonFlow license token carries a JWT-standard aud claim from a closed set of six values. The six values describe a 2 × 3 matrix:
| Hosting mode \ Customer scope | Plugin | SDK | Full |
|---|---|---|---|
SaaS (we operate the agent — try.getaxonflow.com) | axonflow.saas.plugin | axonflow.saas.sdk | axonflow.saas.full |
| Self-Hosted (you operate the agent — Docker / In-VPC) | axonflow.self_hosted.plugin | axonflow.self_hosted.sdk | axonflow.self_hosted.full |
Each cell is a quadrant — a distinct product surface with its own pricing, validation context, and feature set. The matrix is closed and architecturally complete from day one. Future product launches add config (which aud to set at issuance, what tier to bind) — not architecture.
Current status: Self-Hosted x Full remains the classic Enterprise deployment. The SaaS x Plugin quadrant remains active only for unexpired legacy prepaid tokens. The current Hosted Starter subscription authorizes plugins, SDKs, HTTP, and MCP from server-side subscription state, so it does not issue or require a new
X-License-Token.
This page explains how the matrix maps to actual customer behaviour.
Why the matrix exists
Before this design AxonFlow had a single notion of "license tier": Community / Evaluation / Professional / Enterprise / EnterprisePlus. A token's purpose was inferred from its tier name and where it ended up. That worked when there was only one product axis, but the legacy SaaS prepaid offer and future scoped products needed an explicit purpose:
- A legacy SaaS prepaid token pasted into
AXONFLOW_LICENSE_KEYon a self-hosted agent would silently fail closed because the agent saw an unknown tier, but the failure mode was opaque. - A self-hosted Enterprise license sent as
X-License-Tokenon the SaaS agent would similarly fail with no clear reason. - A plugin-only-scope token sent on a request to a future SDK-only product surface had no validation distinguishing it from a full-scope token.
The matrix solves this by making the token's purpose explicit in its aud claim instead of inferred from context. Each validation context — SaaS Plugin path, future SaaS SDK path, self-hosted license loader — keeps an explicit accept list of aud values it recognises, and rejects everything else with an explicit reason.
How a request flows through the matrix
When a plugin or SDK calls the hosted endpoint with an unexpired legacy prepaid token, the hosted auth path does this:
- Resolve
tenant_idfrom the Basic-auth credential (existing behaviour —cs_<uuid>:<secret>). - Read
X-Axonflow-Clientto derive the request's scope. Examples:openclaw/2.1.0→pluginscopecursor-plugin/1.1.0→pluginscopesdk-typescript/7.8.0→sdkscope- (header absent) →
fullscope (default)
- Read
X-License-Tokenif present:- Validate the Ed25519 signature against the SaaS Plugin signing key.
- Check the token's
audis in the SaaS Plugin path's accept list (axonflow.saas.plugin,axonflow.saas.full). Reject with 401cross_quadrant_tokenotherwise. - Check the token's
HasScope(<derived-scope>)returns true. A SaaS Plugin token withaud=axonflow.saas.pluginmatchespluginscope but notsdkscope; aaxonflow.saas.fulltoken matches any scope. Reject with 401scope_mismatchotherwise. - Look up
plugin_user_licensesrow by JTI; requirerevoked_at IS NULLand not-yet-expired. - Resolve the request's effective tier to the row's tier (
ProorPremium).
- If no
X-License-Token, the active Hosted Starter subscription and its tenant-bound allowance decide access. Registration alone does not authorize a governed operation. - Downstream handlers read the resolved entitlement and apply the configured hosted limits.
The X-Axonflow-Client derivation is honest scope enforcement, not a security boundary. A determined caller can spoof the header. The aud-based rejection at the validator is the actual cheating-resistance; the header is for the honest 99%. Same posture as DRM in any one-time-payment product.
The six quadrants today
Self-Hosted × Full (axonflow.self_hosted.full)
The classic AxonFlow Enterprise quadrant. Customer operates the agent in their own VPC; tier comes from AXONFLOW_LICENSE_KEY env var read at startup. Tiers in this quadrant: Community / Evaluation / Professional / Enterprise / EnterprisePlus, per the self-hosted licensing doc.
Tokens issued by the keygen CLI. Validity typically 90 days for the Evaluation tier, 1–2 years for paid tiers, by default with keygen -aud axonflow.self_hosted.full (the unchanged default).
SaaS x Plugin (axonflow.saas.plugin): legacy prepaid tier
This quadrant supported the retired one-time 90-day offer. Existing unexpired and unrevoked tokens keep working through their recorded expiry. It is not sold to new buyers. New hosted access uses the recurring Hosted Starter entitlement bound to the authenticated tenant.
Tier limits:
| Resource | Free | Pro | Premium (planned) |
|---|---|---|---|
| Audit retention | 3 days | 30 days | 90 days |
| Daily event quota | 200 | 2000 | 5000 |
Legacy tokens were issued by the in-agent Stripe webhook handler with the plugin-claim Ed25519 signing key, separate from Evaluation and Enterprise keys. Only the issuer holds the signing key; agents hold the verifier-only public key (AXONFLOW_PLUGIN_CLAIMED_PUBLIC_KEY).
Self-Hosted × Plugin (axonflow.self_hosted.plugin)
The future Plugin In-VPC product — privacy-sensitive small companies running OpenClaw or another plugin-based agent runtime in their own VPC. Skeleton already in the keygen (keygen -aud axonflow.self_hosted.plugin); product launch + tier ladder + pricing are demand-driven, not date-driven.
Reserved future quadrants
- SaaS × SDK (
axonflow.saas.sdk) — when SDK-only pricing is greenlit. - SaaS × Full (
axonflow.saas.full) — when B2B SaaS is greenlit. - Self-Hosted × SDK (
axonflow.self_hosted.sdk) — when SDK-only Self-Hosted pricing is greenlit.
All three validators ship from day one with their accept lists in place — adding the product is a config change (which aud to set at issuance, what Stripe Product to wire), not an architecture change.
How HasScope actually works
The HasScope(scope) helper lives on ServiceLicensePayload. It checks set membership: a token with aud=axonflow.*.full matches any scope query (because full means "no scope restriction"). That gives forward compatibility — adding axonflow.*.http later is a constants-table append, not a validator-code change.
token aud | HasScope("plugin") | HasScope("sdk") | HasScope("full")
axonflow.saas.plugin | ✅ true | ❌ false | ❌ false
axonflow.saas.sdk | ❌ false | ✅ true | ❌ false
axonflow.saas.full | ✅ true | ✅ true | ✅ true
axonflow.self_hosted.full | ✅ true | ✅ true | ✅ true
(no aud — legacy fallback) | ✅ true | ✅ true | ✅ true (treated as self_hosted_full)
Cross-quadrant misuse — for example a SaaS Plugin Pro token sent to the self-hosted license loader — fails earlier, at the accept-list check, before HasScope is consulted.
Backward compat for legacy tokens
Existing self-hosted Enterprise / Evaluation tokens issued before ADR-050 carry empty aud. The validator falls back to treating them as axonflow.self_hosted.full — the only quadrant they could plausibly belong to. No production breakage at upgrade time.
The fallback is removed at the next major key rotation when all in-flight tokens carry explicit aud.
What this means for integrators
Most integrators do not need to think about the matrix. Plugins and SDKs set X-Axonflow-Client automatically, and the agent validates X-License-Token only when a legacy prepaid token is present. A 401 with cross_quadrant_token or scope_mismatch identifies token misuse. Current Hosted Starter customers use subscription state and the service credential instead.
If you're issuing your own tokens (e.g. via the keygen CLI for a self-hosted Enterprise rollout) the only thing to know is:
- Default
audisaxonflow.self_hosted.full— that matches what you want unless you're explicitly issuing for the future Plugin In-VPC or SDK product. - The
keygen -aud <value>flag accepts onlyaxonflow.self_hosted.{plugin,sdk,full}— cross-quadrant SaaS values are rejected at issuance. - Tokens issued for SaaS quadrants come from the in-agent Stripe webhook with a different signing key entirely; you don't issue them yourself.
References
- Auth and header matrix — wire-level reference for
X-Axonflow-Client+X-License-Token - Self-hosted licensing — Self-Hosted × Full quadrant tier ladder
- SDK Authentication — what the SDK sets automatically vs what you configure
