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 andjq. - 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#
cp .env.example .env
Set at least:
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:
openssl ecparam -genkey -name prime256v1 -noout | openssl pkcs8 -topk8 -nocrypt
2. Deploy the contracts#
just deploy-testnet "$RPC"
Deploys MockUSDC + GrantorRegistry and configures the Hobby/Pro/Scale tiers. Note the logged addresses; put them in .env:
CONTRACT_ADDR=0x<GrantorRegistry>
USDC=0x<MockUSDC>
(Optional) verify on the explorer for transparency:
cd contracts && forge verify-contract $CONTRACT_ADDR src/GrantorRegistry.sol:GrantorRegistry \
--chain base-sepolia --watch # needs an explorer API key
3. Run the issuer#
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:
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:
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 + topUps MockUSDC, drawPeriods to Active, and registerIssuerKeys 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:
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):
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):
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#
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).