# Testnet deployment runbook

> **Internal proof runbook — not a public offering.** Grantor's public
> product is the sovereign tier (no server). This runbook exists to prove
> the full stack end-to-end on a testnet; see
> docs/strategy/2026-07-30-sovereign-only-positioning.md.

Deploy the whole Grantor stack — contract, tiers, a tenant, and the live issuer
— to an L2 testnet. **Cost ≈ $0** (testnet gas from a faucet; MockUSDC minted
free). ~20 minutes.

Example chain below: **Base Sepolia** (`RPC=https://sepolia.base.org`). Any EVM
testnet works (Arbitrum/OP Sepolia, Ethereum Sepolia).

## 0. Prerequisites

- **Foundry** (`forge`, `cast`, `anvil`) and **Docker** and **`jq`**.
- A **deployer key** funded with a little testnet ETH:
  - Create one: `cast wallet new` (save the key + address).
  - Fund it from a faucet — Coinbase Developer Platform, Alchemy, or a PoW
    faucet for your chain. (Some gate on a tiny *mainnet* balance; pick one that
    doesn't, or park ~$2 of mainnet ETH in the wallet as a gate — unspent.)
- An **RPC URL** (public, or Alchemy/Infura free tier).

## 1. Configure `.env`

```bash
cp .env.example .env
```

Set at least:

```bash
PRIVATE_KEY=0x<deployer key>        # deployer == registry owner (needed for tier config)
RPC=https://sepolia.base.org
DOMAIN=http://localhost:8080        # your issuer URL (a tunnel later; localhost for now)
TENANT_ID=1                         # the FIRST createTenant returns id 1
TENANT_SALT=<long random secret>
CLIENTS_FILE=./clients.toml         # cp clients.example.toml clients.toml and edit
# TREASURY / GRACE_WINDOW optional — TREASURY defaults to the deployer.
```

Generate the issuer signing key and paste it into `ISSUER_SIGNING_KEY`:

```bash
openssl ecparam -genkey -name prime256v1 -noout | openssl pkcs8 -topk8 -nocrypt
```

## 2. Deploy the contracts

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

Deploys **MockUSDC** + **GrantorRegistry** and configures the **Hobby/Pro/Scale**
tiers. Note the logged addresses; put them in `.env`:

```bash
CONTRACT_ADDR=0x<GrantorRegistry>
USDC=0x<MockUSDC>
```

(Optional) verify on the explorer for transparency:
```bash
cd contracts && forge verify-contract $CONTRACT_ADDR src/GrantorRegistry.sol:GrantorRegistry \
  --chain base-sepolia --watch   # needs an explorer API key
```

## 3. Run the issuer

```bash
just docker-build
docker run -d --name grantor --env-file .env \
  -e CONTRACT_ADDR=$CONTRACT_ADDR \
  -v $PWD/clients.toml:/etc/grantor/clients.toml:ro \
  -p 8080:8080 grantor-issuer
```

It boots without touching the chain. Sanity:
```bash
curl -s localhost:8080/healthz                                  # -> ok
curl -s localhost:8080/.well-known/openid-configuration | jq .  # issuer metadata
```

## 4. Register your tenant + issuer key

Grab the issuer key id from the running issuer, then stand up the tenant:

```bash
export ISSUER_KEY_ID=$(just issuer-keyid http://localhost:8080)   # the JWKS kid == on-chain keyId
echo "ISSUER_KEY_ID=$ISSUER_KEY_ID" >> .env
just setup-tenant "$RPC"
```

This `createTenant` (id **1**), mints + `topUp`s MockUSDC, `drawPeriod`s to
**Active**, and `registerIssuerKey`s your key. The log shows `issuer key active
true`. Since `.env` already has `TENANT_ID=1`, the issuer is now fully live — its
`isActive` reads will return true.

## 5. (Grant C) Register an agent key

For agent tokens, enroll the agent's address:
```bash
cast send $CONTRACT_ADDR "registerAgentKey(uint256,address)" 1 0x<agentAddress> \
  --rpc-url "$RPC" --private-key "$PRIVATE_KEY"
```

## 6. Validate end-to-end

**Agent token (Grant C)** — with the Python SDK (the agent key = the address you
enrolled):
```python
from grantor_sdk import get_agent_token, verify_token
tok = get_agent_token(issuer="http://localhost:8080", tenant_id=1,
                      audience="resource-api", secret_key_hex="<agent key>")
claims = verify_token(tok["access_token"], issuer="http://localhost:8080", audience="resource-api")
print(claims.sub, claims.tid)   # pairwise sub, tid == 1
```

This exercises the full path: SDK signs → issuer verifies the agent key **on
your live testnet contract** → mints → SDK verifies via JWKS.

**Billing lifecycle** (optional): let the period lapse, watch `isActive` go
`Active → Grace → Inactive`, then `topUp` + `drawPeriod` to reactivate. The
issuer picks up the change within its cache TTL.

## 7. Public URL (optional)

For a shareable demo, expose the issuer over HTTPS with a free tunnel and set
`DOMAIN` to the tunnel URL (then restart the container so SIWE/domain checks
match):
```bash
cloudflared tunnel --url http://localhost:8080    # or: ngrok http 8080
```

## Anonymity notes

- Fund the deployer key from a **fresh** source; don't reuse a doxxed wallet.
- Host/domain/RPC accounts should preserve operator pseudonymity if that's the
  goal (this is the place to rehearse it).
- The git identity is still `grantor-dev` — rename before pushing anything.

## Teardown

```bash
docker rm -f grantor
```

The testnet contracts stay on-chain (harmless). Redeploy anytime with
`just deploy-testnet`.

## → Production

Same flow on an L2 **mainnet**, with two changes: point `USDC` at the chain's
**real** USDC (don't deploy MockUSDC; drop the `mint` in SetupTenant and `topUp`
from your balance), and transfer registry ownership to a multisig/timelock after
tier config. See the cost estimate in the design discussion (~$10–30 one-time +
~$10–40/mo; the on-chain fees are net-positive at real usage).
