# Enterprise dedicated deployment runbook

> **Operator-facing.** This is what *you*, the operator, run to stand up and
> administer one enterprise's dedicated registry. The enterprise's own SDK
> configuration is [Enterprise registries](../guide/enterprise-registry.md) —
> read that page for `Registry.dedicated(address, license)` and everything
> downstream of the address + license handoff.

## 1. What this is

An enterprise buys a **dedicated deployment of the Grantor registry**: you
deploy `GrantorRegistryDedicated` once, keeping `owner` (your multisig) and
`treasury` (your Safe), and hand the enterprise exactly **two artifacts** —
the contract address and a signed registry license scoped to
`(chainId, address)`. The enterprise points `Registry.dedicated(address,
license)` at that address in its own verifiers and agents, and administers
its own tenancy on the contract (tenant creation, funding, agent keys, ZK
trees, origin vouches) exactly as on the shared registry.

**You own the venue; the enterprise owns its tenancy; the license is the
ticket that lets their SDKs point at the venue at all.** No account, no
credentials on either side — onboarding is an address and a file.

`GrantorRegistryDedicated` is identical to the shared `GrantorRegistry` in
every tenant-facing behavior, plus one additional owner power: a timelocked,
cancelable kill-switch (§5). Design record:
`docs/superpowers/specs/2026-08-10-dedicated-deployment-design.md`.

## 2. Deploy

```bash
just deploy-dedicated "$RPC"
```

Equivalent, if you're not using `just`:

```bash
cd contracts && forge script script/DeployDedicated.s.sol --rpc-url "$RPC" --broadcast -vv
```

This deploys **one** `GrantorRegistryDedicated` and configures **one**
enterprise tier (tier `1`) with the negotiated numbers. There is no trial
tier and no default tier config — a dedicated deploy happens at deal time,
when the negotiated numbers already exist.

### Required env — no defaults, deal-time only

| Var | Meaning |
|---|---|
| `PRIVATE_KEY` | Deployer key. Also the owner unless `OWNER` is set, so tier config runs in the same broadcast. |
| `KILL_NOTICE_SECS` | The kill-switch notice window. **Ctor reverts under `604800` (7 days)** — that floor is the entire point of §5. Typical negotiated value: 30 days (`2592000`). |
| `TIER_FEE` | Fee in USDC (6 decimals) per billing period. |
| `TIER_MAX_APPS` | Max apps per tenant. |
| `TIER_MAX_ISSUER_KEYS` | Max issuer keys per tenant (irrelevant to the sovereign path, but the same tier-config shape as the shared registry — leave it a sane number). |
| `TIER_MAX_AGENTS` | Max `agent-zk` registrations. |
| `TIER_MAX_ADMINS` | Max tenant admins. |
| `TIER_MAX_USERS` | Max `user-zk` registrations. |

None of these have a fallback. If any is unset the script reverts on the
missing env var — deliberately; there is no "reasonable default" for a
number that was supposed to come out of a signed agreement.

### Optional env

| Var | Default | Notes |
|---|---|---|
| `TREASURY` | none | **Required off a local devnet chain id** (`31337`/`1337`) — the script refuses to guess it. `treasury` is *immutable*; this must be the operator Safe, never a hot deploy key. |
| `OWNER` | the deployer | Should be the operator multisig for anything real; the deployer key is fine only for the local proof run. |
| `GRACE_WINDOW` | `3 days` (`259200`) | Grace after a billing period lapses, before `status()` reports the tenant `Inactive` for billing reasons — unrelated to the kill-switch. |
| `USDC` | deploys a fresh `MockUSDC` **on a devnet chain id only** | On any other chain id, an unset `USDC` is refused — point it at the chain's real USDC. |
| `TIER_PERIOD_SECS` | `30 days` (`2592000`) | Length of one billing period. |

The console output logs `USDC`, the new registry address, `treasury`,
`owner`, and `killNotice`. Save the registry address — it's one of the two
artifacts you hand off.

## 3. Onboard the enterprise

The enterprise's own admin key does this, on the dedicated contract, exactly
as on the shared registry (no MockUSDC minting off a devnet chain — this is
their real USDC balance):

```bash
cast send $REGISTRY "createTenant(address,uint8)" $ENTERPRISE_ADMIN 1 \
  --private-key $ENTERPRISE_ADMIN_KEY --rpc-url "$RPC"
# -> onchain tenant id, typically 1 (the first tenant on a fresh dedicated deploy)

cast send $USDC "approve(address,uint256)" $REGISTRY $FUND_AMOUNT \
  --private-key $ENTERPRISE_ADMIN_KEY --rpc-url "$RPC"
cast send $REGISTRY "topUp(uint256,uint256)" $TENANT_ID $FUND_AMOUNT \
  --private-key $ENTERPRISE_ADMIN_KEY --rpc-url "$RPC"
cast send $REGISTRY "drawPeriod(uint256)" $TENANT_ID \
  --private-key $ENTERPRISE_ADMIN_KEY --rpc-url "$RPC"   # -> status Active
```

You (the operator) issue the license, ledgered so you know what you've
promised and to whom:

```bash
grantor-license issue --key operator-license.key --licensee "Acme Corp" \
  --chain $CHAIN_ID --registry $REGISTRY \
  --expires-days 365 --grace-days 30
```

Hand the printed JSON to the enterprise as `license.json` — that, plus the
address, is the entire handoff. The enterprise configures
`Registry.dedicated($REGISTRY, license)` in its own verifiers and agents; see
[Enterprise registries § Using it](../guide/enterprise-registry.md#using-it)
for the exact call in all four languages. From here their tenancy is theirs
to administer — agent/user registration, origin vouches, issuer keys (if any)
— identically to the shared registry, just on their own contract instance.

## 4. Steady state

- **Billing renews itself.** `drawPeriod` is `external` with no access
  control — anyone (a keeper, a cron job, the enterprise itself) can call it
  once a period lapses, and it draws from the tenant's prepaid balance. You
  run nothing persistent for this.
- **License renewal cadence** is driven off `grantor-license status
  --ledger <path>` — it prints `state` per issued license (`ok` /
  `expiring_soon` / `grace` / `lapsed`), so you know who to contact before a
  license lapses into a `LicenseExpired` refusal on their side (see
  [Enterprise registries § Runtime semantics](../guide/enterprise-registry.md#runtime-semantics-grace-then-hard-refusal)).
- **Point `grantor-watch` at the address** on both sides — its admin-set,
  billing, agent/user-registry and origin-epoch drift detection ([Chain
  watcher](../guide/chain-watcher.md)) works unmodified on a dedicated
  deployment, because `GrantorRegistryDedicated` inherits every one of those
  events verbatim.

⚠️ **`grantor-watch` does not decode `KillAnnounced`/`KillCanceled` today** —
those events exist only on `GrantorRegistryDedicated`, not the shared
`GrantorRegistry` interface `grantor-watch` was built against. Until that's
added, watch the kill-switch directly:

```bash
cast logs --address $REGISTRY --rpc-url "$RPC" "KillAnnounced(uint64)"
cast logs --address $REGISTRY --rpc-url "$RPC" "KillCanceled()"
```

The enterprise should run this (or an equivalent poller) themselves — the
whole point of §5 is that the announcement is visible on-chain the instant it
lands, independent of anything the operator tells them.

## 5. The kill procedure

`GrantorRegistryDedicated` adds one owner power the shared registry does not
have: `announceKill()` / `cancelKill()`, gated by an **immutable** notice
window (`killNotice`, floor 7 days).

| Step | Call | Effect |
|---|---|---|
| Announce | `announceKill()` (owner-only) | `killEffectiveAt = block.timestamp + killNotice`; emits `KillAnnounced(effectiveAt)`. Visible on-chain instantly. `status()` is **unchanged** during the window — every tenant stays exactly as `Active`/`Grace`/`Inactive` as it already was. |
| Cure | `cancelKill()` (owner-only) | `killEffectiveAt = 0`; emits `KillCanceled()`. Fully restores — a re-`announceKill()` afterward starts the **full** notice window over again, never a shortened one. |
| Maturity | (automatic, no call) | Once `block.timestamp >= killEffectiveAt`, `status()` returns `Inactive` for **every** tenant on the instance, overriding whatever their individual billing state was. Every verifier that reads it fails closed with `TenantInactive` (`401` — see [Errors](../guide/errors.md)). |

```bash
cast send $REGISTRY "announceKill()" --private-key $OWNER_KEY --rpc-url "$RPC"
cast call  $REGISTRY "killEffectiveAt()(uint64)" --rpc-url "$RPC"
cast send $REGISTRY "cancelKill()"   --private-key $OWNER_KEY --rpc-url "$RPC"   # cure
```

Two things worth being explicit about:

- **The notice period cannot be shortened after the fact.** `killNotice` is
  set once, in the constructor, and is immutable for the life of the
  contract — the contractual notice clause becomes code the buyer's own
  monitoring can watch, not a promise you could quietly revise.
- **Non-custody survives a completed kill.** `withdrawBalance` checks only
  tenant-admin authorization, never `status()` — a killed tenant's undrawn,
  unconsumed prepaid balance stays withdrawable to an address the tenant
  admin chooses. A kill stops future authentication; it does not lock
  anyone's money — undrawn balance stays withdrawable at every stage.

### Enterprise action: protecting prepaid balance during a kill

When the enterprise sees `KillAnnounced` on-chain, they should take two steps
to protect their undrawn balance:

**Step 1: Disable auto-renewal.**

`drawPeriod` is permissionless and will keep drawing `tenantFee` per period to
the operator's immutable treasury **even after a kill matures** — charging for
service that will not be rendered. Call `setAutoRenew(false)` (tenant-admin-gated)
to stop the automatic draws:

```bash
cast send $REGISTRY "setAutoRenew(uint256,bool)" $TENANT_ID false \
  --private-key $ENTERPRISE_ADMIN_KEY --rpc-url "$RPC"
```

**Step 2: Withdraw undrawn balance once you stop needing periods.**

After the kill matures and authentication stops, retrieve any remaining prepaid
balance. First, query the current balance:

```bash
cast call $REGISTRY "tenants(uint256)" $TENANT_ID
```

This returns a tuple `(tier, periodEnd, balance, appCount, agentCount, userCount)`;
the third field is `balance` (the withdrawable amount in USDC accounting units).
Then withdraw it:

```bash
cast send $REGISTRY "withdrawBalance(uint256,uint256,address)" $TENANT_ID \
  $BALANCE_AMOUNT $RECIPIENT_ADDRESS \
  --private-key $ENTERPRISE_ADMIN_KEY --rpc-url "$RPC"
```

**Operator side:** once `KillAnnounced` lands, the operator should stop (or scale
down) any keeper that calls `drawPeriod` on that deployment — the announced kill
makes every future period worthless to the tenant, so continuing to draw is
billing churn without service.

**Second cure path:** if the kill is announced and then matures, a fresh
`announceKill()` call (re-announcing) will restart the notice window in full,
during which `status()` returns to its billing-derived value and service resumes
— effectively an operator-side undo that lasts until the new maturity. `cancelKill()`
remains the true cure.

## 6. Sub-model B (bespoke only)

Everything above is **sub-model A**: a dedicated contract on the same public
L2 you already deploy on, with real USDC `drawPeriod` billing — both levers
(license routine, on-chain kill) are real there, because the chain's
consensus is nobody's to censor.

**Sub-model B** — the enterprise's own chain, or a permissioned chain they
control — is **bespoke only, and stays that way on purpose.** On a chain the
enterprise controls, *they* control consensus, so an on-chain kill is
illusory: they can simply decline to include the transaction that would
enforce it. `status()` failing closed only means something when the read
itself is trustworthy. B's real levers are the license grant and the signed
agreement, full stop — no on-chain enforcement is claimed for it and none
should be. **No B tooling exists**, deliberately: building a kill-switch that
doesn't actually constrain the party it's aimed at would be worse than no
tooling at all — it would look like a control instead of being one.

## 7. No pricing numbers anywhere

Every number in this runbook (`TIER_FEE`, the caps, `KILL_NOTICE_SECS`,
license `--expires-days`/`--grace-days`) is a deal-time parameter, decided
per enterprise and passed as an env var or CLI flag at deploy/issue time.
Nothing here — or anywhere else in this repo — publishes what any of those
numbers should be. Pricing is deferred entirely to the first deal.

## See also

- [Enterprise registries](../guide/enterprise-registry.md) — the SDK-side
  `RegistryRef`/license mechanics this runbook hands off into.
- [Chain watcher](../guide/chain-watcher.md) — admin/billing drift detection, and the `KillAnnounced` gap noted in §4.
- [Testnet deployment runbook](testnet.md) — the shared-registry equivalent of this page.
- [Errors](../guide/errors.md) — `TenantInactive`, `LicenseExpired`, and every other code a verifier can return.
