<p align="center"><img src=".github/hero.svg" alt="plans" width="880"></p>

# @hanzo/plans

Canonical plan + pricing definitions for the Hanzo platform. Single source of truth, consumed by every product surface that renders a price tag or gates a feature.

## Install

```sh
npm install @hanzo/plans
```

## Usage

```js
import {
  subscriptionPlans, // free / dev / max / team / enterprise
  dnsPlans,          // dns-free / dns-pro / dns-enterprise
  cloudPlans,        // starter / builder / dev / pro / turbo / ...
  blockchainPlans,
  gpuTiers,
  regions,
  seats,
  storage,
  tools,
  pricingPolicy,
} from '@hanzo/plans'

console.log(subscriptionPlans.find((p) => p.id === 'max'))
// → { id: 'max', name: 'Max', priceMonthly: 99, priceAnnual: 81.18, ... }
```

Or import a single JSON file:

```js
import subscription from '@hanzo/plans/subscription.json'
import dns from '@hanzo/plans/dns.json'
```

## Entitlements — the canonical vocabulary (keystone)

Entitlements used to be defined three times: plan `features` were display strings,
plan `limits` were ad-hoc machine keys, commerce duplicated them, and licensing
needed its own machine keys for the engine. This package now defines the **one**
machine-readable entitlement vocabulary, here, once.

- **`entitlements.schema.json`** — JSON-Schema (draft 2020-12) of the namespaced
  vocabulary. Every key is `namespace.key` with a fixed type + `x-unit`, across
  nine namespaces: `ai.*`, `cloud.*`, `licensing.*`, `world.*`, `dns.*`, `rpc.*`,
  `data.*`, `tools.*`, `commerce.*`. Headline keys: `ai.tokens_per_min`,
  `ai.models`, `cloud.max_vms`, `cloud.gpu_class`, `licensing.app_ids`,
  `licensing.seats`. A numeric `-1` means **unlimited**; `null` means unset/inherit.
- **`plan.schema.json`** — the plan envelope. Adds **`tenant_id`** (so each
  reseller/org has its own catalog), the typed **`entitlements`** block, and the
  **`price_ref`** block. Display `features` and legacy `limits` remain optional.
- **`entitlements.mjs`** — runtime helpers (`ENTITLEMENT_KEYS`, `fromLegacy`,
  `toPriceRefStub`, `toLicenseFeatures`, `resolvePlan`).

### `tenant_id` — multi-tenant catalogs

Plan ids are **no longer globally unique**. The unique key is the pair
`(tenant_id, id)`. `tenant_id` defaults to `"hanzo"` (the first-party catalog)
when omitted, so every existing record stays valid. A reseller ships its own plan
rows under its own `tenant_id` without colliding with `hanzo`'s `pro`/`max`/etc.

### The data contract: `entitlements` → `price_ref` → license `features`

```
plan.entitlements          (machine keys; the source of truth)
   │  fromLegacy(plan)      ← derives entitlements from legacy limits/addons/payouts
   ▼
price_ref                  (pricing.hanzo.ai)
   ├─ recurring             flat subscription line (monthly/annual, per_seat)
   └─ metered[]             usage meters: { entitlement, unit, source, included }
                            resolved live vs OpenRouter / HF / Zen gateway / DO
   │  toLicenseFeatures(entitlements)
   ▼
license token `features`   (hanzoai/licensing → the proprietary engine)
                            flat []string the engine's release gate matches
                            (releases.go hasFeatures): "inference", "training",
                            "ai.premium", "licensing.app:hanzo",
                            "licensing.product:engine", "deploy.on_prem", …
```

- **commerce** reads `entitlements` to populate its `Entitlement` (members, SSO,
  product ids, seats) instead of re-deriving from display strings.
- **licensing** copies `licensing.engine_features` plus the derived capability
  tokens into the signed token `features`; the engine verifies them offline.
- Quantitative quotas (tokens/min, seats, VMs) ride the token as structured
  limits, not as feature strings, keeping the engine's string-set check meaningful.

```js
import { resolvePlan, ENTITLEMENT_KEYS, fromLegacy, toLicenseFeatures } from '@hanzo/plans'

const max = subscriptionPlans.find((p) => p.id === 'max')
resolvePlan(max)
// → { tenant_id: 'hanzo', id: 'max', entitlements: {...}, price_ref: {...},
//     license_features: ['ai.premium','training','licensing.app:hanzo', ...] }
```

## Schema

### Subscription plans (`subscription.json`)

```ts
type Plan = {
  tenant_id?: string          // owning reseller/org; defaults to 'hanzo'.
                              // unique key is (tenant_id, id)
  id: string                  // 'pro', 'world-pro', 'team-max', ... (unique per tenant)
  name: string
  description: string
  priceMonthly: number | null // legacy convenience price; authoritative is price_ref
  priceAnnual: number | null  // USD per month billed annually
  category: 'personal' | 'team' | 'enterprise' | 'world'
  popular?: boolean
  contactSales?: boolean
  features: string[]          // DISPLAY strings — do not gate on these
  limits?: {                  // DEPRECATED: back-compat source for `entitlements`
    requestsPerMinute?: number
    tokensPerMinute?: number
    freeCredit?: number
    maxMembers?: number
    maxAlerts?: number
    apiRateLimit?: number
    mcpRateLimit?: number
  }
  entitlements?: Entitlements // CANONICAL machine truth (see entitlements.schema.json)
  price_ref?: PriceRef        // billing binding: { currency, recurring, metered[] }
  bundles?: string[]    // slugs of plans this plan also grants free
  includedIn?: string[] // slugs of parent plans that bundle this one
  payouts?: { idleResalePercent: number; description: string }
}
```

### Bundles

Several Hanzo platform tiers grant free access to a Hanzo World tier without a second charge:

| Platform plan | Bundles |
|---|---|
| `max` | `world-pro` |
| `team` / `enterprise` | `world-team` |

A subscription created against a parent plan should mint zero-priced child subscriptions for every entry in `bundles`. Hanzo Commerce does this automatically in its `CreateBillingSubscription` handler.

### Agent runtime (`seats.json`)

Agent compute is billed by the hour, not by the seat:

| | Rate |
|---|---|
| `runtime.agentHourUSD` | $0.08 / hour |

One rate covers all agent runtime — agentic coding, chat inference sessions, and a bot running resident. Hours are metered on top of the subscription.

What a tier includes is a **capacity**, on the tier: `limits.agents` and `limits.bots` (canonically `ai.agents` / `ai.bots`). Free may run 1 agent, `dev` 10, `max` 10 plus 1 resident bot, `enterprise` unlimited (`-1`). Those cap how many may exist; they are not an allowance of hours, so a consumer needs both halves and must not read one as the other.

`0` means the tier may run none of that kind and is a real, enforceable answer; a missing key means nobody has said yet, and must not be read as zero.

## Contributing

`hanzoai/plans` is the only canonical source. Every consumer (commerce, pricing, console, cloud, hanzo.ai marketing) reads this package — never their own copy. PRs against this repo cascade out via npm publish + downstream image rebuilds.

## License

Apache 2.0 — see `LICENSE`. The data itself is customer-facing pricing; the licence covers the JavaScript bindings.
